Repository: linkerd/linkerd2
Pull request #4145 (2020-03-06 16:04:29)

Update inject to handle `automountServiceAccountToken: false`
Fixes https://github.com/linkerd/linkerd2/issues/3183 
Fixes https://github.com/linkerd/linkerd2/issues/2843

When a pod spec contains `automountServiceAccountToken: false` :
- `inject` throws an error
- ~webhook emits an `InjectionSkip` event with the message `Linkerd sidecar proxy injection skipped: automountServiceAccountToken set to "false"`~
```bash
$ cat deploy.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      automountServiceAccountToken: false
      containers:
      - name: nginx
        image: nginx
        ports:
        - name: http
          containerPort: 80

$ cat deploy.yaml | bin/go-run cli inject -
Error transforming resources: cannot enable mTLS when automountServiceAccountToken set to "false"
```

Unit tests have been added to exercise the newly added code.

Signed-off-by: Mayank Shah <mayankshah1614@gmail.com>



ihcsim: The output of the `linkerd inject` command looks a bit off:

```
bin/linkerd version                          
Client version: git-590100a3
Server version: git-590100a3

cat <<EOF| bin/linkerd inject -
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      automountServiceAccountToken: false
      containers:
      - name: nginx
        image: nginx
        ports:
        - name: http
          containerPort: 80
EOF

‼ "linkerd.io/inject: disabled" annotation set on deployment/nginx
‼ no supported objects found

deployment "nginx" skipped
```

If I piped that into `kubectl apply`, the deployment is still created as an uninjected workload. Can we return an error and not create the workload instead?

```
cat <<EOF| bin/linkerd inject - | k apply -f -     (kubernetes-admin@linkerd/default)
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      automountServiceAccountToken: false
      containers:
      - name: nginx
        image: nginx
        ports:
        - name: http
          containerPort: 80
EOF

kubectl get po                                           
NAME                     READY   STATUS    RESTARTS   AGE
nginx-5dd74fff98-85cv4   1/1     Running   0          10s
```
mayankshah1607: @ihcsim From the issue discussion thread from https://github.com/linkerd/linkerd2/issues/3183
> @mayankshah1607 The only part that we can do here is to return an error and emit an event when automountServiceAccountToken: false, which implies that mTLS won't work. 

If we just throw an error and not create any workload, that would mean that the webhook wouldn't get triggered, hence not throwing an `InjectionSkip` event, right?
mayankshah1607: @ihcsim Done! The output of `inject` now looks like this
```bash
$ cat deploy.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      automountServiceAccountToken: false
      containers:
      - name: nginx
        image: nginx
        ports:
        - name: http
          containerPort: 80

$ cat deploy.yaml | bin/go-run cli inject -
Error transforming resources: disabled_automount_service_account_token_account
```
As mentioned earlier, now that the workload is not created, it will not trigger the webhook to emit `InjectionSkip` events. Let me know if this works. I've also updated the tests.

Patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 163f2200d..1a910a26e 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -16,6 +16,8 @@ MAINTAINERS.md @klingerf @olix0r
 
 /pkg @adleong @alpeb
 
+Dockerfile* @adleong @alpeb @klingerf @olix0r @siggy
+
 # These do not have clear owners today
 # /grafana
 # /proto
diff --git a/.github/workflows/kind_integration.yml b/.github/workflows/kind_integration.yml
index a8fea9b52..641b903b7 100644
--- a/.github/workflows/kind_integration.yml
+++ b/.github/workflows/kind_integration.yml
@@ -142,20 +142,8 @@ jobs:
         PROXY_INIT_IMAGE_NAME: gcr.io/linkerd-io/proxy-init:v1.3.1
         PROMETHEUS_IMAGE_NAME: prom/prometheus:v2.15.2
       run: |
-        # For each container, load the image archive into the KinD cluster.
-        #
-        # `kind load` cannot take input from STDIN, so `<(command)` syntax is
-        # used to load the output into the KinD cluster. Set `DOCKER_HOST` for
-        # a single command, and `docker save` the container from the Packet
-        # host.
-        for image in proxy controller web cni-plugin debug grafana; do
-          kind load image-archive <(DOCKER_HOST=ssh://linkerd-docker docker save "$DOCKER_REGISTRY/$image:$TAG") || tee load_fail &
-        done
-
-        # Wait for `kind load` background processes to complete. Exit early if
-        # any job failed.
-        wait < <(jobs -p)
-        test -f load_fail && exit 1 || true
+        # Fetch images from the Packet host and load them into the local KinD cluster
+        bin/kind-load --images --images-host ssh://linkerd-docker
 
         # Load proxy-init and prometheus images into KinD while it is
         # available. Allow these commands to fail since they will be cached
@@ -165,14 +153,7 @@ jobs:
     - name: Load image archives into the local KinD cluster (Forked repositories)
       if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
       run: |
-        for image in proxy controller web cni-plugin debug grafana; do
-          kind load image-archive image-archives/$image.tar || tee load_fail &
-        done
-
-        # Wait for `kind load` background processes to complete. Exit early if
-        # any job failed.
-        wait < <(jobs -p)
-        test -f load_fail && exit 1 || true
+        bin/kind-load --images
     - name: Run integration tests
       run: |
         # Export `init_test_run` and `*_integration_tests` into the
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 010165911..995ddade4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -140,20 +140,8 @@ jobs:
         PROXY_INIT_IMAGE_NAME: gcr.io/linkerd-io/proxy-init:v1.3.1
         PROMETHEUS_IMAGE_NAME: prom/prometheus:v2.15.2
       run: |
-        # For each container, load the image archive into the KinD cluster.
-        #
-        # `kind load` cannot take input from STDIN, so `<(command)` syntax is
-        # used to load the output into the KinD cluster. Set `DOCKER_HOST` for
-        # a single command, and `docker save` the container from the Packet
-        # host.
-        for image in proxy controller web cni-plugin debug grafana; do
-          kind load image-archive <(DOCKER_HOST=ssh://linkerd-docker docker save "$DOCKER_REGISTRY/$image:$TAG") || tee load_fail &
-        done
-
-        # Wait for `kind load` background processes to complete. Exit early if
-        # any job failed.
-        wait < <(jobs -p)
-        test -f load_fail && exit 1 || true
+        # Fetch images from the Packet host and load them into the local KinD cluster
+        bin/kind-load --images --images-host ssh://linkerd-docker
 
         # Load proxy-init and prometheus images into KinD while it is
         # available. Allow these commands to fail since they will be cached
diff --git a/.golangci.yml b/.golangci.yml
index 29a408181..6b2cf3f0c 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -7,6 +7,8 @@
 
 run:
   deadline: 5m
+  skip-dirs:
+  - controller/gen
 issues:
   exclude-use-default: false
 linters:
diff --git a/.proxy-version b/.proxy-version
index 4b2170c01..3750c20ca 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.89.0
+v2.90.0
diff --git a/ADOPTERS.md b/ADOPTERS.md
index 0695a9104..45d7b23b7 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -21,6 +21,7 @@
 - [Paybase](https://paybase.io/)
 - [PlayStudios Asia](https://www.playstudios.asia)
 - [Studyo](https://studyo.co)
+- [Transit](https://transit.app)
 - [Vernacular.ai](https://vernacular.ai/)
 
 If you're using Linkerd 2.x and aren't on this list, please [submit a pull
diff --git a/CHANGES.md b/CHANGES.md
index a88cbb05c..91539f064 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,40 @@
+## edge-20.3.4
+
+This release introduces several fixes and improvements to the CLI.
+
+* CLI
+  * Added support for kubectl-style label selectors in many CLI commands (thanks
+    @mayankshah1607!)
+  * Fixed the path regex in service profiles generated from proto files without
+    a package name (thanks @amariampolskiy!)
+  * Fixed an error when injecting Cronjobs that have no metadata
+  * Relaxed the clock skew check to match the default node heartbeat interval
+    on Kubernetes 1.17 and made this check a warning
+  * Fixed a bug where the linkerd-smi-metrics pod could not be created on
+    clusters with pod security policy enabled
+* Internal
+  * Upgraded tracing components to more recent versions and improved resource
+    defaults (thanks @Pothulapati!)
+
+## edge-20.3.3
+
+This release introduces new experimental CLI commands for querying metrics using
+the Service Mesh Interface (SMI) and for multi-cluster support via service
+mirroring.
+
+If you would like to learn more about service mirroring or SMI, or are
+interested in experimenting with these features, please join us in
+[Linkerd Slack](https://slack.linkerd.io) for help and feedback.
+
+* CLI
+  * Added experimental `linkerd cluster` commands for managing multi-cluster
+    service mirroring
+  * Added the experimental `linkerd alpha clients` command, which uses the
+    smi-metrics API to display client-side metrics from each of a resource's
+    clients
+  * Added retries to some `linkerd check` checks to prevent spurious failures
+    when run immediately after cluster creation or Linkerd installation
+
 ## edge-20.3.2
 
 This release introduces substantial proxy improvements as well as
diff --git a/Dockerfile-proxy b/Dockerfile-proxy
index fd1a9a508..b758d67db 100644
--- a/Dockerfile-proxy
+++ b/Dockerfile-proxy
@@ -9,7 +9,7 @@ RUN (proxy=$(bin/fetch-proxy $(cat proxy-version)) && \
     mv "$proxy" linkerd2-proxy)
 
 ## compile proxy-identity agent
-FROM gcr.io/linkerd-io/go-deps:6b9a8ea4 as golang
+FROM gcr.io/linkerd-io/go-deps:1b3f62c3 as golang
 WORKDIR /linkerd-build
 COPY pkg/flags pkg/flags
 COPY pkg/tls pkg/tls
diff --git a/bin/create-release-tag b/bin/create-release-tag
index ab95ff9c4..0a36b8406 100755
--- a/bin/create-release-tag
+++ b/bin/create-release-tag
@@ -15,7 +15,7 @@ fi
 release_channel="$1"
 
 # Verify the release channel.
-release_channel_regex="(edge|stable)"
+release_channel_regex="^(edge|stable)$"
 if [[ ! $release_channel =~ $release_channel_regex ]]; then
     echo "Error: valid release channels: edge, stable"
     echo "Usage:"
diff --git a/bin/install-pr b/bin/install-pr
index c8c32cd72..829c0b52a 100755
--- a/bin/install-pr
+++ b/bin/install-pr
@@ -95,19 +95,7 @@ then
     name=$(echo "$context" | sed -n -E "s/(kind)-(.*)/\2/p")
   fi
 
-  for image in cni-plugin controller debug grafana proxy web
-  do
-    "$bindir"/kind load image-archive ${name:+'--name' "$name"} $image.tar || touch load_fail &
-  done
-
-  # Wait for `kind load` background processes to complete; exit early if any
-  # job failed
-  wait < <(jobs -p)
-  if [ -f load_fail ]
-  then
-    echo "Loading docker images into KinD cluster failed."
-    exit 1
-  fi
+  "$bindir"/kind-load --images ${name:+'--name' "$name"}
 else
   for image in cni-plugin controller debug grafana proxy web
   do
diff --git a/bin/kind-load b/bin/kind-load
index 0ff4e52ac..f4cde59dc 100755
--- a/bin/kind-load
+++ b/bin/kind-load
@@ -1,17 +1,87 @@
-#!/bin/sh
+#!/bin/bash
 
-set -eu
+set -eo pipefail
 
-# If cluster name is unset or null, default to $USER.
-cluster=${1:-$USER}
+images=""
+images_host=""
+
+while :
+do
+  case $1 in
+    -h|--help)
+      echo "Load into KinD the images for Linkerd's proxy, controller, web, grafana, cli-bin, debug and cni-plugin."
+      echo ""
+      echo "Usage:"
+      echo "    bin/kind-load [--images] [--images-host ssh://linkerd-docker]"
+      echo ""
+      echo "Examples:"
+      echo ""
+      echo "    # Load images from the local docker instance"
+      echo "    bin/kind-load"
+      echo ""
+      echo "    # Load images from tar files located in the current directory"
+      echo "    bin/kind-load --images"
+      echo ""
+      echo "    # Retrieve images from a remote docker instance and then load them into KinD"
+      echo "    bin/kind-load --images --images-host ssh://linkerd-docker"
+      echo ""
+      echo "Available Commands:"
+      echo "    --images: use 'kind load image-archive' to load the images from local .tar files in the current directory."
+      echo "    --images-host: the argument to this option is used as the remote docker instance from which images are first retrieved"
+      echo "                   (using 'docker save') to be then loaded into KinD. This command requires --images."
+      exit 0
+      ;;
+    --images)
+      images=1
+      ;;
+    --images-host)
+      images_host=$2
+      if [ -z "$images_host" ]; then
+        echo "Error: the argument for --images-host was not specified"
+        exit 1
+      fi
+      shift
+      ;;
+    *)
+      break
+  esac
+  shift
+done
+
+if [ "$images_host" ] && [ -z "$images" ]; then
+  echo "Error: --images-host needs to be used with --images" >&2
+  exit 1
+fi
+
+# If cluster name is unset or null, default to "kind".
+cluster=${1:-"kind"}
 
 bindir=$( cd "${0%/*}" && pwd )
 
+# shellcheck source=bin/_tag.sh
 . "$bindir"/_tag.sh
+# shellcheck source=bin/_docker.sh
+. "$bindir"/_docker.sh
+
+TAG=${TAG:-$(head_root_tag)}
 
-tag=$(head_root_tag)
-org_repo=gcr.io/linkerd-io
+rm -f load_fail
+for img in proxy controller web grafana cli-bin debug cni-plugin ; do
+  if [ $images ]; then
+    if [ "$images_host" ]; then
+      DOCKER_HOST=$images_host docker save "$DOCKER_REGISTRY/$img:$TAG" > $img.tar
+    fi
+    cmd=(image-archive "$img.tar")
+  else
+    cmd=(docker-image "$DOCKER_REGISTRY/$img:$TAG")
+  fi
 
-for img in proxy controller web grafana ; do
-    "$bindir"/kind load docker-image "$org_repo/$img:$tag" --name $cluster
+  "$bindir"/kind --name "$cluster" load "${cmd[@]}" || touch load_fail &
 done
+
+wait < <(jobs -p)
+if [ -f load_fail ]; then
+  echo "Loading docker images into KinD cluster failed."
+  rm load_fail
+  exit 1
+fi
diff --git a/bin/lint b/bin/lint
index 877646f02..1f89be279 100755
--- a/bin/lint
+++ b/bin/lint
@@ -2,7 +2,7 @@
 
 set -eu
 
-lintversion=1.19.1
+lintversion=1.23.8
 
 cd "$(pwd -P)"
 
diff --git a/charts/linkerd2/templates/psp.yaml b/charts/linkerd2/templates/psp.yaml
index 98613c80d..1c3fb3b9c 100644
--- a/charts/linkerd2/templates/psp.yaml
+++ b/charts/linkerd2/templates/psp.yaml
@@ -116,3 +116,6 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: {{.Values.global.namespace}}
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: {{.Values.global.namespace}}
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index 575e773f3..c58927922 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -170,7 +170,7 @@ nodeSelector:
   beta.kubernetes.io/os: linux
 
 smiMetrics:
-  image: deislabs/smi-metrics:v0.2.0
+  image: deislabs/smi-metrics:v0.2.1
   # if empty, Helm will auto-generate these fields
   crtPEM: |
 
@@ -181,15 +181,9 @@ tracing:
   enabled: false
   collector:
     name: linkerd-collector
-    image: omnition/opencensus-collector:0.1.10
+    image: omnition/opencensus-collector:0.1.11
     resources:
-      cpu:
-        limit: 1
-        request: 200m
-      memory:
-        limit: 2Gi
-        request: 400Mi
   jaeger:
     name: linkerd-jaeger
-    image: jaegertracing/all-in-one:1.8
+    image: jaegertracing/all-in-one:1.17.1
     resources:
diff --git a/charts/patch/templates/patch.json b/charts/patch/templates/patch.json
index 38864ec65..6f8703e95 100644
--- a/charts/patch/templates/patch.json
+++ b/charts/patch/templates/patch.json
@@ -1,5 +1,12 @@
 {{ $prefix := .Values.pathPrefix -}}
 [
+  {{- if .Values.addRootMetadata }}
+  {
+    "op": "add",
+    "path": "{{$prefix}}/metadata",
+    "value": {}
+  },
+  {{- end }}
   {{- if .Values.addRootAnnotations }}
   {
     "op": "add",
diff --git a/cli/Dockerfile-bin b/cli/Dockerfile-bin
index fe5542c4c..32957f83e 100644
--- a/cli/Dockerfile-bin
+++ b/cli/Dockerfile-bin
@@ -1,5 +1,5 @@
 ## compile binaries
-FROM gcr.io/linkerd-io/go-deps:6b9a8ea4 as golang
+FROM gcr.io/linkerd-io/go-deps:1b3f62c3 as golang
 WORKDIR /linkerd-build
 COPY cli cli
 COPY charts charts
diff --git a/cli/cmd/cluster.go b/cli/cmd/cluster.go
new file mode 100644
index 000000000..1b35566bd
--- /dev/null
+++ b/cli/cmd/cluster.go
@@ -0,0 +1,298 @@
+package cmd
+
+import (
+	"fmt"
+	"strings"
+
+	v1 "k8s.io/api/rbac/v1"
+
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/spf13/cobra"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/yaml"
+
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/clientcmd/api"
+)
+
+const (
+	tokenKey                  = "token"
+	defaultServiceAccountName = "linkerd-service-mirror"
+	defaultServiceAccountNs   = "default"
+	defaultClusterName        = "remote"
+)
+
+type (
+	getCredentialsOptions struct {
+		namespace           string
+		serviceAccount      string
+		clusterName         string
+		remoteClusterDomain string
+	}
+
+	createOptions struct {
+		namespace      string
+		serviceAccount string
+	}
+
+	exportServiceOptions struct {
+		namespace        string
+		service          string
+		gatewayNamespace string
+		gatewayName      string
+	}
+)
+
+func newCmdCluster() *cobra.Command {
+
+	getOpts := getCredentialsOptions{}
+	createOpts := createOptions{}
+	exportOpts := exportServiceOptions{}
+	clusterCmd := &cobra.Command{
+
+		Hidden: true,
+		Use:    "cluster [flags]",
+		Args:   cobra.NoArgs,
+		Short:  "Manages the multicluster setup for Linkerd",
+		Long: `Manages the multicluster setup for Linkerd.
+
+This command provides subcommands to manage the multicluster support
+functionality of Linkerd. You can use it to deploy credentials to
+remote clusters, extract them as well as export remote services to be
+available across clusters.`,
+		Example: `  # Create remote cluster credentials.
+  linkerd --context=cluster-a cluster create-credentials | kubectl --context=cluster-a apply -f -
+
+  # Extract mirroring cluster credentials from cluster A and install them on cluster B
+  linkerd --context=cluster-a cluster get-credentials --cluster-name=remote | kubectl apply --context=cluster-b -f -
+
+  # Export service from cluster A to be available to other clusters
+  linkerd --context=cluster-a cluster export-service --service-name=backend-svc --service-namespace=default --gateway-name=linkerd-gateway --gateway-ns=default`,
+	}
+
+	createCredentialsCommand := &cobra.Command{
+		Hidden: false,
+		Use:    "create-credentials",
+		Short:  "Create the necessary credentials for service mirroring",
+		Args:   cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+
+			labels := map[string]string{
+				k8s.ControllerComponentLabel: k8s.ServiceMirrorLabel,
+				k8s.ControllerNSLabel:        controlPlaneNamespace,
+			}
+
+			clusterRole := v1.ClusterRole{
+				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
+				TypeMeta:   metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"},
+				Rules: []rbacv1.PolicyRule{
+					{
+						APIGroups: []string{""},
+						Resources: []string{"services"},
+						Verbs:     []string{"list", "get", "watch"},
+					},
+				},
+			}
+
+			svcAccount := corev1.ServiceAccount{
+				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
+				TypeMeta:   metav1.TypeMeta{Kind: v1.ServiceAccountKind, APIVersion: "v1"},
+			}
+
+			clusterRoleBinding := v1.ClusterRoleBinding{
+				TypeMeta:   metav1.TypeMeta{Kind: "ClusterRoleBinding", APIVersion: "rbac.authorization.k8s.io/v1"},
+				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
+
+				Subjects: []v1.Subject{
+					v1.Subject{Kind: v1.ServiceAccountKind, Name: createOpts.serviceAccount, Namespace: createOpts.namespace},
+				},
+				RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: createOpts.serviceAccount},
+			}
+
+			crOut, err := yaml.Marshal(clusterRole)
+			if err != nil {
+				return err
+			}
+
+			saOut, err := yaml.Marshal(svcAccount)
+			if err != nil {
+				return err
+			}
+			crbOut, err := yaml.Marshal(clusterRoleBinding)
+			if err != nil {
+				return err
+			}
+			fmt.Println(fmt.Sprintf("---\n%s---\n%s---\n%s", crOut, saOut, crbOut))
+			return nil
+		},
+	}
+
+	getCredentialsCmd := &cobra.Command{
+		Hidden: false,
+		Use:    "get-credentials",
+		Short:  "Get cluster credentials as a secret",
+		Args:   cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			rules := clientcmd.NewDefaultClientConfigLoadingRules()
+			rules.ExplicitPath = kubeconfigPath
+			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
+			config, err := loader.RawConfig()
+			if err != nil {
+				return err
+			}
+
+			if kubeContext != "" {
+				config.CurrentContext = kubeContext
+			}
+
+			k, err := k8s.NewAPI(kubeconfigPath, config.CurrentContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return err
+			}
+
+			sa, err := k.CoreV1().ServiceAccounts(getOpts.namespace).Get(getOpts.serviceAccount, metav1.GetOptions{})
+			if err != nil {
+				return err
+			}
+
+			var secretName string
+			for _, s := range sa.Secrets {
+				if strings.HasPrefix(s.Name, fmt.Sprintf("%s-token", sa.Name)) {
+					secretName = s.Name
+					break
+				}
+			}
+			if secretName == "" {
+				return fmt.Errorf("could not find service account token secret for %s", sa.Name)
+			}
+
+			secret, err := k.CoreV1().Secrets(getOpts.namespace).Get(secretName, metav1.GetOptions{})
+			if err != nil {
+				return err
+			}
+
+			token, ok := secret.Data[tokenKey]
+			if !ok {
+				return fmt.Errorf("could not find the token data in the service account secret")
+			}
+
+			context, ok := config.Contexts[config.CurrentContext]
+			if !ok {
+				return fmt.Errorf("could not extract current context from config")
+			}
+
+			context.AuthInfo = getOpts.serviceAccount
+			config.Contexts = map[string]*api.Context{
+				config.CurrentContext: context,
+			}
+			config.AuthInfos = map[string]*api.AuthInfo{
+				getOpts.serviceAccount: {
+					Token: string(token),
+				},
+			}
+
+			cluster := config.Clusters[context.Cluster]
+
+			config.Clusters = map[string]*api.Cluster{
+				context.Cluster: cluster,
+			}
+
+			kubeconfig, err := clientcmd.Write(config)
+			if err != nil {
+				return err
+			}
+
+			creds := corev1.Secret{
+				Type:     k8s.MirrorSecretType,
+				TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: fmt.Sprintf("cluster-credentials-%s", getOpts.clusterName),
+					Annotations: map[string]string{
+						k8s.RemoteClusterNameLabel:        getOpts.clusterName,
+						k8s.RemoteClusterDomainAnnotation: getOpts.remoteClusterDomain,
+					},
+				},
+				Data: map[string][]byte{
+					k8s.ConfigKeyName: kubeconfig,
+				},
+			}
+
+			out, err := yaml.Marshal(creds)
+			if err != nil {
+				return err
+			}
+			fmt.Println(string(out))
+
+			return nil
+		},
+	}
+
+	exportServiceCmd := &cobra.Command{
+		Hidden: false,
+		Use:    "export-service",
+		Short:  "Exposes a remote service to be mirrored",
+		Args:   cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			rules := clientcmd.NewDefaultClientConfigLoadingRules()
+			rules.ExplicitPath = kubeconfigPath
+			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
+			config, err := loader.RawConfig()
+			if err != nil {
+				return err
+			}
+
+			if kubeContext != "" {
+				config.CurrentContext = kubeContext
+			}
+
+			k, err := k8s.NewAPI(kubeconfigPath, config.CurrentContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return err
+			}
+
+			svc, err := k.CoreV1().Services(exportOpts.namespace).Get(exportOpts.service, metav1.GetOptions{})
+			if err != nil {
+				return err
+			}
+
+			_, hasGatewayName := svc.Annotations[k8s.GatewayNameAnnotation]
+			_, hasGatewayNs := svc.Annotations[k8s.GatewayNsAnnotation]
+
+			if hasGatewayName || hasGatewayNs {
+				return fmt.Errorf("service %s/%s has already been exported", svc.Namespace, svc.Name)
+			}
+
+			svc.Annotations[k8s.GatewayNameAnnotation] = exportOpts.gatewayName
+			svc.Annotations[k8s.GatewayNsAnnotation] = exportOpts.gatewayNamespace
+
+			_, err = k.CoreV1().Services(svc.Namespace).Update(svc)
+			if err != nil {
+				return err
+			}
+
+			fmt.Println(fmt.Sprintf("Service %s/%s is now exported", svc.Namespace, svc.Name))
+			return nil
+		},
+	}
+	getCredentialsCmd.Flags().StringVar(&getOpts.serviceAccount, "service-account-name", defaultServiceAccountName, "the name of the service account")
+	getCredentialsCmd.Flags().StringVar(&getOpts.namespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account will be created")
+	getCredentialsCmd.Flags().StringVar(&getOpts.clusterName, "cluster-name", defaultClusterName, "cluster name")
+	getCredentialsCmd.Flags().StringVar(&getOpts.remoteClusterDomain, "remote-cluster-domain", defaultClusterDomain, "custom remote cluster domain")
+
+	createCredentialsCommand.Flags().StringVar(&createOpts.serviceAccount, "service-account-name", defaultServiceAccountName, "the name of the service account used")
+	createCredentialsCommand.Flags().StringVar(&createOpts.namespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account can be found")
+
+	exportServiceCmd.Flags().StringVar(&exportOpts.service, "service-name", "", "the name of the service to be exported")
+	exportServiceCmd.Flags().StringVar(&exportOpts.namespace, "service-namespace", "", "the namespace in which the service to be exported resides")
+	exportServiceCmd.Flags().StringVar(&exportOpts.gatewayName, "gateway-name", "", "the name of the gateway")
+	exportServiceCmd.Flags().StringVar(&exportOpts.gatewayNamespace, "gateway-ns", "", "the ns of the gateway")
+
+	clusterCmd.AddCommand(getCredentialsCmd)
+	clusterCmd.AddCommand(createCredentialsCommand)
+	clusterCmd.AddCommand(exportServiceCmd)
+
+	return clusterCmd
+}
diff --git a/cli/cmd/inject.go b/cli/cmd/inject.go
index c14d0be2c..aa98c847e 100644
--- a/cli/cmd/inject.go
+++ b/cli/cmd/inject.go
@@ -23,12 +23,13 @@ import (
 
 const (
 	// for inject reports
-	hostNetworkDesc    = "pods do not use host networking"
-	sidecarDesc        = "pods do not have a 3rd party proxy or initContainer already injected"
-	injectDisabledDesc = "pods are not annotated to disable injection"
-	unsupportedDesc    = "at least one resource injected"
-	udpDesc            = "pod specs do not include UDP ports"
-	slash              = "/"
+	hostNetworkDesc                  = "pods do not use host networking"
+	sidecarDesc                      = "pods do not have a 3rd party proxy or initContainer already injected"
+	injectDisabledDesc               = "pods are not annotated to disable injection"
+	unsupportedDesc                  = "at least one resource injected"
+	udpDesc                          = "pod specs do not include UDP ports"
+	slash                            = "/"
+	automountServiceAccountTokenDesc = "cannot enable mTLS when automountServiceAccountToken set to \"false\""
 )
 
 type resourceTransformerInject struct {
@@ -165,8 +166,10 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
 		b, err := conf.InjectNamespace(rt.overrideAnnotations)
 		return b, reports, err
 	}
-
 	if b, _ := report.Injectable(); !b {
+		if !report.AutomountServiceAccountToken {
+			return bytes, reports, errors.New(automountServiceAccountTokenDesc)
+		}
 		return bytes, reports, nil
 	}
 
@@ -300,7 +303,7 @@ func (resourceTransformerInject) generateReport(reports []inject.Report, output
 			if r.Kind != "" {
 				output.Write([]byte(fmt.Sprintf("%s \"%s\" skipped\n", r.Kind, r.Name)))
 			} else {
-				output.Write([]byte(fmt.Sprintf("document missing \"kind\" field, skipped\n")))
+				output.Write([]byte(fmt.Sprintln("document missing \"kind\" field, skipped")))
 			}
 		}
 	}
diff --git a/cli/cmd/root.go b/cli/cmd/root.go
index 0873e4839..326689be1 100644
--- a/cli/cmd/root.go
+++ b/cli/cmd/root.go
@@ -25,6 +25,8 @@ const (
 	jsonOutput  = "json"
 	tableOutput = "table"
 	wideOutput  = "wide"
+
+	maxRps = 100.0
 )
 
 var (
@@ -125,6 +127,7 @@ func init() {
 	RootCmd.AddCommand(newCmdUpgrade())
 	RootCmd.AddCommand(newCmdVersion())
 	RootCmd.AddCommand(newCmdInstallServiceMirror())
+	RootCmd.AddCommand(newCmdCluster())
 }
 
 type statOptionsBase struct {
diff --git a/cli/cmd/routes.go b/cli/cmd/routes.go
index d126fd5bf..04ad7d53a 100644
--- a/cli/cmd/routes.go
+++ b/cli/cmd/routes.go
@@ -19,9 +19,10 @@ import (
 
 type routesOptions struct {
 	statOptionsBase
-	toResource   string
-	toNamespace  string
-	dstIsService bool
+	toResource    string
+	toNamespace   string
+	dstIsService  bool
+	labelSelector string
 }
 
 type routeRowStats struct {
@@ -36,6 +37,7 @@ func newRoutesOptions() *routesOptions {
 		statOptionsBase: *newStatOptionsBase(),
 		toResource:      "",
 		toNamespace:     "",
+		labelSelector:   "",
 	}
 }
 
@@ -77,6 +79,7 @@ This command will only display traffic which is sent to a service that has a Ser
 	cmd.PersistentFlags().StringVar(&options.toResource, "to", options.toResource, "If present, shows outbound stats to the specified resource")
 	cmd.PersistentFlags().StringVar(&options.toNamespace, "to-namespace", options.toNamespace, "Sets the namespace used to lookup the \"--to\" resource; by default the current \"--namespace\" is used")
 	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, fmt.Sprintf("Output format; one of: \"%s\", \"%s\", or \"%s\"", tableOutput, wideOutput, jsonOutput))
+	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
 
 	return cmd
 }
@@ -318,6 +321,7 @@ func buildTopRoutesRequest(resource string, options *routesOptions) (*pb.TopRout
 			ResourceType: target.Type,
 			Namespace:    options.namespace,
 		},
+		LabelSelector: options.labelSelector,
 	}
 
 	options.dstIsService = !(target.GetType() == k8s.Authority)
diff --git a/cli/cmd/tap.go b/cli/cmd/tap.go
index 56f259783..003c19d9a 100644
--- a/cli/cmd/tap.go
+++ b/cli/cmd/tap.go
@@ -23,15 +23,16 @@ import (
 type renderTapEventFunc func(*pb.TapEvent, string) string
 
 type tapOptions struct {
-	namespace   string
-	toResource  string
-	toNamespace string
-	maxRps      float32
-	scheme      string
-	method      string
-	authority   string
-	path        string
-	output      string
+	namespace     string
+	toResource    string
+	toNamespace   string
+	maxRps        float32
+	scheme        string
+	method        string
+	authority     string
+	path          string
+	output        string
+	labelSelector string
 }
 
 type endpoint struct {
@@ -102,15 +103,16 @@ type tapEvent struct {
 
 func newTapOptions() *tapOptions {
 	return &tapOptions{
-		namespace:   "default",
-		toResource:  "",
-		toNamespace: "",
-		maxRps:      100.0,
-		scheme:      "",
-		method:      "",
-		authority:   "",
-		path:        "",
-		output:      "",
+		namespace:     "default",
+		toResource:    "",
+		toNamespace:   "",
+		maxRps:        maxRps,
+		scheme:        "",
+		method:        "",
+		authority:     "",
+		path:          "",
+		output:        "",
+		labelSelector: "",
 	}
 }
 
@@ -169,16 +171,17 @@ func newCmdTap() *cobra.Command {
 		ValidArgs: util.ValidTargets,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			requestParams := util.TapRequestParams{
-				Resource:    strings.Join(args, "/"),
-				Namespace:   options.namespace,
-				ToResource:  options.toResource,
-				ToNamespace: options.toNamespace,
-				MaxRps:      options.maxRps,
-				Scheme:      options.scheme,
-				Method:      options.method,
-				Authority:   options.authority,
-				Path:        options.path,
-				Extract:     options.output == jsonOutput,
+				Resource:      strings.Join(args, "/"),
+				Namespace:     options.namespace,
+				ToResource:    options.toResource,
+				ToNamespace:   options.toNamespace,
+				MaxRps:        options.maxRps,
+				Scheme:        options.scheme,
+				Method:        options.method,
+				Authority:     options.authority,
+				Path:          options.path,
+				Extract:       options.output == jsonOutput,
+				LabelSelector: options.labelSelector,
 			}
 
 			err := options.validate()
@@ -218,6 +221,8 @@ func newCmdTap() *cobra.Command {
 		"Display requests with paths that start with this prefix")
 	cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output,
 		fmt.Sprintf("Output format. One of: \"%s\", \"%s\"", wideOutput, jsonOutput))
+	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector,
+		"Selector (label query) to filter on, supports '=', '==', and '!='")
 
 	return cmd
 }
diff --git a/cli/cmd/top.go b/cli/cmd/top.go
index 0708ee3c6..f1532a7e8 100644
--- a/cli/cmd/top.go
+++ b/cli/cmd/top.go
@@ -25,16 +25,17 @@ import (
 )
 
 type topOptions struct {
-	namespace   string
-	toResource  string
-	toNamespace string
-	maxRps      float32
-	scheme      string
-	method      string
-	authority   string
-	path        string
-	hideSources bool
-	routes      bool
+	namespace     string
+	toResource    string
+	toNamespace   string
+	maxRps        float32
+	scheme        string
+	method        string
+	authority     string
+	path          string
+	hideSources   bool
+	routes        bool
+	labelSelector string
 }
 
 type topRequest struct {
@@ -258,16 +259,17 @@ const (
 
 func newTopOptions() *topOptions {
 	return &topOptions{
-		namespace:   "default",
-		toResource:  "",
-		toNamespace: "",
-		maxRps:      100.0,
-		scheme:      "",
-		method:      "",
-		authority:   "",
-		path:        "",
-		hideSources: false,
-		routes:      false,
+		namespace:     "default",
+		toResource:    "",
+		toNamespace:   "",
+		maxRps:        maxRps,
+		scheme:        "",
+		method:        "",
+		authority:     "",
+		path:          "",
+		hideSources:   false,
+		routes:        false,
+		labelSelector: "",
 	}
 }
 
@@ -317,15 +319,16 @@ func newCmdTop() *cobra.Command {
 		ValidArgs: util.ValidTargets,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			requestParams := util.TapRequestParams{
-				Resource:    strings.Join(args, "/"),
-				Namespace:   options.namespace,
-				ToResource:  options.toResource,
-				ToNamespace: options.toNamespace,
-				MaxRps:      options.maxRps,
-				Scheme:      options.scheme,
-				Method:      options.method,
-				Authority:   options.authority,
-				Path:        options.path,
+				Resource:      strings.Join(args, "/"),
+				Namespace:     options.namespace,
+				ToResource:    options.toResource,
+				ToNamespace:   options.toNamespace,
+				MaxRps:        options.maxRps,
+				Scheme:        options.scheme,
+				Method:        options.method,
+				Authority:     options.authority,
+				Path:          options.path,
+				LabelSelector: options.labelSelector,
 			}
 
 			if options.hideSources {
@@ -374,6 +377,7 @@ func newCmdTop() *cobra.Command {
 		"Display requests with paths that start with this prefix")
 	cmd.PersistentFlags().BoolVar(&options.hideSources, "hide-sources", options.hideSources, "Hide the source column")
 	cmd.PersistentFlags().BoolVar(&options.routes, "routes", options.routes, "Display data per route instead of per path")
+	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
 
 	return cmd
 }
diff --git a/cli/cmd/uninject.go b/cli/cmd/uninject.go
index a88c7dc89..d82e5ad23 100644
--- a/cli/cmd/uninject.go
+++ b/cli/cmd/uninject.go
@@ -96,7 +96,7 @@ func (resourceTransformerUninject) generateReport(reports []inject.Report, outpu
 			if r.Kind != "" {
 				output.Write([]byte(fmt.Sprintf("%s \"%s\" skipped\n", r.Kind, r.Name)))
 			} else {
-				output.Write([]byte(fmt.Sprintf("document missing \"kind\" field, skipped\n")))
+				output.Write([]byte(fmt.Sprintln("document missing \"kind\" field, skipped")))
 			}
 		}
 	}
diff --git a/cni-plugin/Dockerfile b/cni-plugin/Dockerfile
index 69e39fbfa..c812258ea 100644
--- a/cni-plugin/Dockerfile
+++ b/cni-plugin/Dockerfile
@@ -1,5 +1,5 @@
 ## compile cni-plugin utility
-FROM gcr.io/linkerd-io/go-deps:6b9a8ea4 as golang
+FROM gcr.io/linkerd-io/go-deps:1b3f62c3 as golang
 WORKDIR /linkerd-build
 COPY pkg pkg
 COPY controller controller
diff --git a/controller/Dockerfile b/controller/Dockerfile
index 21dcac40d..2c6684e4c 100644
--- a/controller/Dockerfile
+++ b/controller/Dockerfile
@@ -1,5 +1,5 @@
 ## compile controller service
-FROM gcr.io/linkerd-io/go-deps:6b9a8ea4 as golang
+FROM gcr.io/linkerd-io/go-deps:1b3f62c3 as golang
 WORKDIR /linkerd-build
 COPY controller/gen controller/gen
 COPY pkg pkg
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index c4acdbc7c..7a1811699 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -49,12 +49,20 @@ func (et *endpointTranslator) Add(set watcher.AddressSet) {
 		if address.Pod != nil {
 			wa, err = et.toWeightedAddr(address)
 		} else {
+			var authOverride *pb.AuthorityOverride
+			if address.AuthorityOverride != "" {
+				authOverride = &pb.AuthorityOverride{
+					AuthorityOverride: address.AuthorityOverride,
+				}
+			}
+
 			// handling address with no associated pod
 			var addr *net.TcpAddress
 			addr, err = et.toAddr(address)
 			wa = &pb.WeightedAddr{
-				Addr:   addr,
-				Weight: defaultWeight,
+				Addr:              addr,
+				Weight:            defaultWeight,
+				AuthorityOverride: authOverride,
 			}
 
 			if address.Identity != "" {
@@ -65,7 +73,7 @@ func (et *endpointTranslator) Add(set watcher.AddressSet) {
 						},
 					},
 				}
-				// in this case we most likely have a proxy on the side, so set protocol hint as well.
+				// in this case we most likely have a proxy on the other side, so set protocol hint as well.
 				if et.enableH2Upgrade {
 					wa.ProtocolHint = &pb.ProtocolHint{
 						Protocol: &pb.ProtocolHint_H2_{
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index fd2f6b450..6dfb400e2 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -31,12 +31,13 @@ type (
 	// case that this endpoint is not associated with a pod and maps
 	// to some other IP (i.e. a remote service gateway)
 	Address struct {
-		IP        string
-		Port      Port
-		Pod       *corev1.Pod
-		OwnerName string
-		OwnerKind string
-		Identity  string
+		IP                string
+		Port              Port
+		Pod               *corev1.Pod
+		OwnerName         string
+		OwnerKind         string
+		Identity          string
+		AuthorityOverride string
 	}
 
 	// AddressSet is a set of Address, indexed by ID.
@@ -90,6 +91,7 @@ type (
 	portPublisher struct {
 		id         ServiceID
 		targetPort namedPort
+		srcPort    Port
 		hostname   string
 		log        *logging.Entry
 		k8sAPI     *k8s.API
@@ -392,6 +394,7 @@ func (sp *servicePublisher) newPortPublisher(srcPort Port, hostname string) *por
 	port := &portPublisher{
 		listeners:  []EndpointUpdateListener{},
 		targetPort: targetPort,
+		srcPort:    srcPort,
 		hostname:   hostname,
 		exists:     exists,
 		k8sAPI:     sp.k8sAPI,
@@ -465,10 +468,16 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) Addre
 					Namespace: endpoints.ObjectMeta.Namespace,
 				}
 
+				var authorityOverride string
+				if fqName, ok := endpoints.Annotations[consts.RemoteServiceFqName]; ok {
+					authorityOverride = fmt.Sprintf("%s:%d", fqName, pp.srcPort)
+				}
+
 				addresses[id] = Address{
-					IP:       endpoint.IP,
-					Port:     resolvedPort,
-					Identity: endpoints.Annotations[consts.RemoteGatewayIdentity],
+					IP:                endpoint.IP,
+					Port:              resolvedPort,
+					Identity:          endpoints.Annotations[consts.RemoteGatewayIdentity],
+					AuthorityOverride: authorityOverride,
 				}
 				continue
 			}
diff --git a/controller/api/public/top_routes.go b/controller/api/public/top_routes.go
index dcd5e34f0..ba21ee51b 100644
--- a/controller/api/public/top_routes.go
+++ b/controller/api/public/top_routes.go
@@ -53,7 +53,10 @@ func (s *grpcServer) TopRoutes(ctx context.Context, req *pb.TopRoutesRequest) (*
 	// TopRoutes will return one table for each resource object requested.
 	tables := make([]resourceTable, 0)
 	targetResource := req.GetSelector().GetResource()
-
+	labelSelector, err := getTopLabelSelector(req)
+	if err != nil {
+		return nil, err
+	}
 	if targetResource.GetType() == k8s.Authority {
 		// Authority cannot be the target because authorities don't have namespaces,
 		// therefore there is no namespace in which to look for a service profile.
@@ -61,7 +64,7 @@ func (s *grpcServer) TopRoutes(ctx context.Context, req *pb.TopRoutesRequest) (*
 	}
 
 	// Non-authority resource
-	objects, err := s.k8sAPI.GetObjects(targetResource.Namespace, targetResource.Type, targetResource.Name, labels.Everything())
+	objects, err := s.k8sAPI.GetObjects(targetResource.Namespace, targetResource.Type, targetResource.Name, labelSelector)
 	if err != nil {
 		return nil, err
 	}
@@ -114,6 +117,10 @@ func (s *grpcServer) topRoutesFor(ctx context.Context, req *pb.TopRoutesRequest,
 	}
 	clientNs := req.GetSelector().GetResource().GetNamespace()
 	typ := req.GetSelector().GetResource().GetType()
+	labelSelector, err := getTopLabelSelector(req)
+	if err != nil {
+		return nil, err
+	}
 	targetResource := &pb.Resource{
 		Name:      name,
 		Namespace: req.GetSelector().GetResource().GetNamespace(),
@@ -128,14 +135,14 @@ func (s *grpcServer) topRoutesFor(ctx context.Context, req *pb.TopRoutesRequest,
 
 	if requestedResource.GetType() == k8s.Authority {
 		// Authorities may not be a source, so we know this is a ToResource.
-		profiles, err = s.getProfilesForAuthority(requestedResource.GetName(), clientNs)
+		profiles, err = s.getProfilesForAuthority(requestedResource.GetName(), clientNs, labelSelector)
 		if err != nil {
 			return nil, err
 		}
 	} else {
 		// Non-authority resource.
 		// Lookup individual resource objects.
-		objects, err := s.k8sAPI.GetObjects(requestedResource.Namespace, requestedResource.Type, requestedResource.Name, labels.Everything())
+		objects, err := s.k8sAPI.GetObjects(requestedResource.Namespace, requestedResource.Type, requestedResource.Name, labelSelector)
 		if err != nil {
 			return nil, err
 		}
@@ -191,10 +198,10 @@ func validateRequest(req *pb.TopRoutesRequest) *pb.TopRoutesResponse {
 	return nil
 }
 
-func (s *grpcServer) getProfilesForAuthority(authority string, clientNs string) (map[string]*sp.ServiceProfile, error) {
+func (s *grpcServer) getProfilesForAuthority(authority string, clientNs string, labelSelector labels.Selector) (map[string]*sp.ServiceProfile, error) {
 	if authority == "" {
 		// All authorities
-		ps, err := s.k8sAPI.SP().Lister().ServiceProfiles(clientNs).List(labels.Everything())
+		ps, err := s.k8sAPI.SP().Lister().ServiceProfiles(clientNs).List(labelSelector)
 		if err != nil {
 			return nil, err
 		}
@@ -347,3 +354,16 @@ func processRouteMetrics(results []promResult, timeWindow string, table indexedT
 		}
 	}
 }
+
+//generate correct label.Selector object according to the request
+func getTopLabelSelector(req *pb.TopRoutesRequest) (labels.Selector, error) {
+	labelSelector := labels.Everything()
+	if s := req.GetSelector().GetLabelSelector(); s != "" {
+		var err error
+		labelSelector, err = labels.Parse(s)
+		if err != nil {
+			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+		}
+	}
+	return labelSelector, nil
+}
diff --git a/controller/api/util/api_utils.go b/controller/api/util/api_utils.go
index 67321497d..0a11879ee 100644
--- a/controller/api/util/api_utils.go
+++ b/controller/api/util/api_utils.go
@@ -94,24 +94,26 @@ type EdgesRequestParams struct {
 // requests.
 type TopRoutesRequestParams struct {
 	StatsBaseRequestParams
-	ToNamespace string
-	ToType      string
-	ToName      string
+	ToNamespace   string
+	ToType        string
+	ToName        string
+	LabelSelector string
 }
 
 // TapRequestParams contains parameters that are used to build a
 // TapByResourceRequest.
 type TapRequestParams struct {
-	Resource    string
-	Namespace   string
-	ToResource  string
-	ToNamespace string
-	MaxRps      float32
-	Scheme      string
-	Method      string
-	Authority   string
-	Path        string
-	Extract     bool
+	Resource      string
+	Namespace     string
+	ToResource    string
+	ToNamespace   string
+	MaxRps        float32
+	Scheme        string
+	Method        string
+	Authority     string
+	Path          string
+	Extract       bool
+	LabelSelector string
 }
 
 // GRPCError generates a gRPC error code, as defined in
@@ -301,6 +303,7 @@ func BuildTopRoutesRequest(p TopRoutesRequestParams) (*pb.TopRoutesRequest, erro
 				Name:      p.ResourceName,
 				Type:      resourceType,
 			},
+			LabelSelector: p.LabelSelector,
 		},
 		TimeWindow: window,
 	}
@@ -513,7 +516,8 @@ func BuildTapByResourceRequest(params TapRequestParams) (*pb.TapByResourceReques
 
 	return &pb.TapByResourceRequest{
 		Target: &pb.ResourceSelection{
-			Resource: &target,
+			Resource:      &target,
+			LabelSelector: params.LabelSelector,
 		},
 		MaxRps: params.MaxRps,
 		Match: &pb.TapByResourceRequest_Match{
diff --git a/controller/cmd/service-mirror/cluster_watcher.go b/controller/cmd/service-mirror/cluster_watcher.go
index 782be2b0f..ffedf91c1 100644
--- a/controller/cmd/service-mirror/cluster_watcher.go
+++ b/controller/cmd/service-mirror/cluster_watcher.go
@@ -76,6 +76,7 @@ type (
 		gatewayData          *gatewayMetadata
 		newResourceVersion   string
 		affectedServices     []*corev1.Service
+		identity             string
 	}
 
 	// ConsiderGatewayUpdateDispatch is issued when we are receiving an update for a
@@ -142,9 +143,9 @@ func (re RetryableError) Error() string {
 	return fmt.Sprintf("Inner errors:\n\t%s", strings.Join(errorStrings, "\n\t"))
 }
 
-func (rcsw *RemoteClusterServiceWatcher) extractGatewayInfo(gateway *corev1.Service) ([]corev1.EndpointAddress, int32, string, error) {
+func (rcsw *RemoteClusterServiceWatcher) extractGatewayInfo(gateway *corev1.Service) ([]corev1.EndpointAddress, int32, string, string, error) {
 	if len(gateway.Status.LoadBalancer.Ingress) == 0 {
-		return nil, 0, "", errors.New("expected gateway to have at lest 1 external Ip address but it has none")
+		return nil, 0, "", "", errors.New("expected gateway to have at lest 1 external Ip address but it has none")
 	}
 
 	var foundPort = false
@@ -158,7 +159,7 @@ func (rcsw *RemoteClusterServiceWatcher) extractGatewayInfo(gateway *corev1.Serv
 	}
 
 	if !foundPort {
-		return nil, 0, "", fmt.Errorf("cannot find  port named %s on gateway", consts.GatewayPortName)
+		return nil, 0, "", "", fmt.Errorf("cannot find  port named %s on gateway", consts.GatewayPortName)
 	}
 
 	var gatewayEndpoints []corev1.EndpointAddress
@@ -168,16 +169,19 @@ func (rcsw *RemoteClusterServiceWatcher) extractGatewayInfo(gateway *corev1.Serv
 			Hostname: ingress.Hostname,
 		})
 	}
-	return gatewayEndpoints, port, gateway.ResourceVersion, nil
+
+	gatewayIdentity := gateway.Annotations[consts.GatewayIdentity]
+
+	return gatewayEndpoints, port, gateway.ResourceVersion, gatewayIdentity, nil
 }
 
 // When the gateway is resolved we need to produce a set of endpoint addresses that that
 // contain the external IPs that this gateway exposes. Therefore we return the IP addresses
 // as well as a single port on which the gateway is accessible.
-func (rcsw *RemoteClusterServiceWatcher) resolveGateway(metadata *gatewayMetadata) ([]corev1.EndpointAddress, int32, string, error) {
+func (rcsw *RemoteClusterServiceWatcher) resolveGateway(metadata *gatewayMetadata) ([]corev1.EndpointAddress, int32, string, string, error) {
 	gateway, err := rcsw.remoteAPIClient.Svc().Lister().Services(metadata.Namespace).Get(metadata.Name)
 	if err != nil {
-		return nil, 0, "", err
+		return nil, 0, "", "", err
 	}
 	return rcsw.extractGatewayInfo(gateway)
 }
@@ -401,7 +405,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceUpdated(ev *RemoteSe
 	serviceInfo := fmt.Sprintf("%s/%s", ev.remoteUpdate.Namespace, ev.remoteUpdate.Name)
 	rcsw.log.Debugf("Updating remote mirrored service %s/%s", ev.localService.Namespace, ev.localService.Name)
 
-	gatewayEndpoints, gatewayPort, resVersion, err := rcsw.resolveGateway(ev.gatewayData)
+	gatewayEndpoints, gatewayPort, resVersion, gatewayIdentity, err := rcsw.resolveGateway(ev.gatewayData)
 	if err == nil {
 		ev.localEndpoints.Subsets = []corev1.EndpointSubset{
 			{
@@ -413,6 +417,12 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceUpdated(ev *RemoteSe
 		ev.localEndpoints.Labels[consts.RemoteGatewayNameLabel] = ev.gatewayData.Name
 		ev.localEndpoints.Labels[consts.RemoteGatewayNsLabel] = ev.gatewayData.Namespace
 
+		if gatewayIdentity != "" {
+			ev.localEndpoints.Annotations[consts.RemoteGatewayIdentity] = gatewayIdentity
+		} else {
+			delete(ev.localEndpoints.Annotations, consts.RemoteGatewayIdentity)
+		}
+
 	} else {
 		rcsw.log.Warnf("Could not resolve gateway for %s: %s, nulling endpoints", serviceInfo, err)
 		ev.localEndpoints.Subsets = nil
@@ -479,11 +489,14 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ev *RemoteSe
 				consts.RemoteGatewayNameLabel: ev.gatewayData.Name,
 				consts.RemoteGatewayNsLabel:   ev.gatewayData.Namespace,
 			},
+			Annotations: map[string]string{
+				consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", remoteService.Name, remoteService.Namespace, rcsw.clusterDomain),
+			},
 		},
 	}
 
 	// Now we try to resolve the remote gateway
-	gatewayEndpoints, gatewayPort, resVersion, err := rcsw.resolveGateway(ev.gatewayData)
+	gatewayEndpoints, gatewayPort, resVersion, gatewayIdentity, err := rcsw.resolveGateway(ev.gatewayData)
 	if err == nil {
 		// only if we resolve it, we are updating the endpoints addresses and ports
 		rcsw.log.Debugf("Resolved remote gateway [%v:%d] for %s", gatewayEndpoints, gatewayPort, serviceInfo)
@@ -493,9 +506,10 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ev *RemoteSe
 				Ports:     rcsw.getEndpointsPorts(ev.service, gatewayPort),
 			},
 		}
-
 		serviceToCreate.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = resVersion
-
+		if gatewayIdentity != "" {
+			endpointsToCreate.Annotations[consts.RemoteGatewayIdentity] = gatewayIdentity
+		}
 	} else {
 		rcsw.log.Warnf("Could not resolve gateway for %s: %s, skipping subsets", serviceInfo, err)
 		endpointsToCreate.Subsets = nil
@@ -570,6 +584,13 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayUpdated(ev *RemoteGa
 				Ports:     rcsw.getEndpointsPorts(updatedService, ev.newPort),
 			},
 		}
+
+		if ev.identity != "" {
+			updatedEndpoints.Annotations[consts.RemoteGatewayIdentity] = ev.identity
+		} else {
+			delete(updatedEndpoints.Annotations, consts.RemoteGatewayIdentity)
+		}
+
 		_, err = rcsw.localAPIClient.Client.CoreV1().Services(updatedService.Namespace).Update(updatedService)
 		if err != nil {
 			errors = append(errors, err)
@@ -624,7 +645,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleConsiderGatewayUpdateDispatch(eve
 			Name:      event.maybeGateway.Name,
 			Namespace: event.maybeGateway.Namespace,
 		}
-		if endpoints, port, resVersion, err := rcsw.extractGatewayInfo(event.maybeGateway); err != nil {
+		if endpoints, port, resVersion, identity, err := rcsw.extractGatewayInfo(event.maybeGateway); err != nil {
 			rcsw.log.Warnf("Gateway [%s/%s] is not a compliant gateway anymore, dispatching GatewayDeleted event: %s", event.maybeGateway.Namespace, event.maybeGateway.Name, err)
 			// in case something changed about this gateway and it is not really a gateway anymore,
 			// simply dispatch deletion event so all endpoints are nulled
@@ -646,6 +667,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleConsiderGatewayUpdateDispatch(eve
 					gatewayData:          gatewayMeta,
 					newResourceVersion:   resVersion,
 					affectedServices:     affectedServices,
+					identity:             identity,
 				})
 			}
 
diff --git a/controller/tap/server.go b/controller/tap/server.go
index dab120eaa..607da7cec 100644
--- a/controller/tap/server.go
+++ b/controller/tap/server.go
@@ -59,6 +59,10 @@ func (s *GRPCTapServer) TapByResource(req *public.TapByResourceRequest, stream p
 		return status.Error(codes.InvalidArgument, "TapByResource received nil target ResourceSelection")
 	}
 	res := req.GetTarget().GetResource()
+	labelSelector, err := getLabelSelector(req)
+	if err != nil {
+		return err
+	}
 	if res == nil {
 		return status.Error(codes.InvalidArgument, "TapByResource received nil target Resource")
 	}
@@ -66,7 +70,7 @@ func (s *GRPCTapServer) TapByResource(req *public.TapByResourceRequest, stream p
 		req.MaxRps = defaultMaxRps
 	}
 
-	objects, err := s.k8sAPI.GetObjects(res.GetNamespace(), res.GetType(), res.GetName(), labels.Everything())
+	objects, err := s.k8sAPI.GetObjects(res.GetNamespace(), res.GetType(), res.GetName(), labelSelector)
 	if err != nil {
 		return apiUtil.GRPCError(err)
 	}
@@ -651,3 +655,15 @@ func (s *GRPCTapServer) resourceForIP(ip *public.IPAddress) (runtime.Object, err
 
 	return singleRunningPod, nil
 }
+
+func getLabelSelector(req *public.TapByResourceRequest) (labels.Selector, error) {
+	labelSelector := labels.Everything()
+	if s := req.GetTarget().GetLabelSelector(); s != "" {
+		var err error
+		labelSelector, err = labels.Parse(s)
+		if err != nil {
+			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+		}
+	}
+	return labelSelector, nil
+}
diff --git a/go.mod b/go.mod
index 230c5509b..4d1761920 100644
--- a/go.mod
+++ b/go.mod
@@ -31,7 +31,7 @@ require (
 	github.com/huandu/xstrings v1.2.0 // indirect
 	github.com/imdario/mergo v0.3.7
 	github.com/julienschmidt/httprouter v1.2.0
-	github.com/linkerd/linkerd2-proxy-api v0.1.10
+	github.com/linkerd/linkerd2-proxy-api v0.1.12
 	github.com/linkerd/linkerd2-proxy-init v1.3.2
 	github.com/mattn/go-isatty v0.0.9
 	github.com/mattn/go-runewidth v0.0.2
diff --git a/go.sum b/go.sum
index 57e1e409a..f13c3cfc2 100644
--- a/go.sum
+++ b/go.sum
@@ -211,6 +211,10 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/linkerd/linkerd2-proxy-api v0.1.10 h1:zGwz2Fek7fdfCxyZW7jq8IE6sexeVFW5UFBQFL6MrP4=
 github.com/linkerd/linkerd2-proxy-api v0.1.10/go.mod h1:2WJHEYXoww5ALM6c1QspRFiROGZg08tGxCO1js0tTVA=
+github.com/linkerd/linkerd2-proxy-api v0.1.12-0.20200309112816-75a7b3b20597 h1:HIzyna4iqxvaBgLVicTvoiBJjWlgqe28iBDygTG14lA=
+github.com/linkerd/linkerd2-proxy-api v0.1.12-0.20200309112816-75a7b3b20597/go.mod h1:2WJHEYXoww5ALM6c1QspRFiROGZg08tGxCO1js0tTVA=
+github.com/linkerd/linkerd2-proxy-api v0.1.12 h1:mP0UYjmemLI6Ru7m1HW3d9up5Bt9VRB9FsxRA8jy2IM=
+github.com/linkerd/linkerd2-proxy-api v0.1.12/go.mod h1:2WJHEYXoww5ALM6c1QspRFiROGZg08tGxCO1js0tTVA=
 github.com/linkerd/linkerd2-proxy-init v1.3.1 h1:KTvUvPlSKUkpzZM0L/EIk+EJaKk7dwygu0CP95fOyGI=
 github.com/linkerd/linkerd2-proxy-init v1.3.1/go.mod h1:lK/sORPZDP6aFhPZps+AHMKphX5v9bKVDNESllz18C0=
 github.com/linkerd/linkerd2-proxy-init v1.3.2 h1:bl+gwdBtHI0cki/urTyicxxb/ekQtVWJ6NDWzMXAgVs=
diff --git a/grafana/dashboards/route.json b/grafana/dashboards/route.json
new file mode 100644
index 000000000..3e6ea96a3
--- /dev/null
+++ b/grafana/dashboards/route.json
@@ -0,0 +1,1262 @@
+{
+    "annotations": {
+      "list": [
+        {
+          "builtIn": 1,
+          "datasource": "-- Grafana --",
+          "enable": true,
+          "hide": true,
+          "iconColor": "rgba(0, 211, 255, 1)",
+          "name": "Annotations & Alerts",
+          "type": "dashboard"
+        }
+      ]
+    },
+    "editable": true,
+    "gnetId": null,
+    "graphTooltip": 1,
+    "id": null,
+    "iteration": 1539806914987,
+    "links": [],
+    "panels": [
+      {
+        "content": "<div style=\"display: flex; align-items: center\">\n  <img src=\"https://linkerd.io/images/identity/favicon/linkerd-favicon.png\" style=\"height:32px;\"/>&nbsp;\n  <span style=\"font-size: 32px\">route/$rt_route</span>\n</div>",
+        "gridPos": {
+          "h": 2,
+          "w": 24,
+          "x": 0,
+          "y": 0
+        },
+        "id": 2,
+        "links": [],
+        "mode": "html",
+        "options": {},
+        "title": "",
+        "transparent": true,
+        "type": "text"
+      },
+      {
+        "cacheTimeout": null,
+        "colorBackground": false,
+        "colorValue": false,
+        "colors": [
+          "#d44a3a",
+          "rgba(237, 129, 40, 0.89)",
+          "#299c46"
+        ],
+        "datasource": "prometheus",
+        "decimals": null,
+        "format": "percentunit",
+        "gauge": {
+          "maxValue": 1,
+          "minValue": 0,
+          "show": true,
+          "thresholdLabels": false,
+          "thresholdMarkers": true
+        },
+        "gridPos": {
+          "h": 4,
+          "w": 8,
+          "x": 0,
+          "y": 2
+        },
+        "id": 4,
+        "interval": null,
+        "links": [],
+        "mappingType": 1,
+        "mappingTypes": [
+          {
+            "name": "value to text",
+            "value": 1
+          },
+          {
+            "name": "range to text",
+            "value": 2
+          }
+        ],
+        "maxDataPoints": 100,
+        "nullPointMode": "connected",
+        "nullText": null,
+        "options": {},
+        "postfix": "",
+        "postfixFontSize": "50%",
+        "prefix": "",
+        "prefixFontSize": "50%",
+        "rangeMaps": [
+          {
+            "from": "null",
+            "text": "N/A",
+            "to": "null"
+          }
+        ],
+        "sparkline": {
+          "fillColor": "rgba(31, 118, 189, 0.18)",
+          "full": true,
+          "lineColor": "rgb(31, 120, 193)",
+          "show": true
+        },
+        "tableColumn": "",
+        "targets": [
+          {
+            "expr": "sum(irate(route_response_total{classification=\"success\", namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s])) / sum(irate(route_response_total{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s]))",
+            "format": "time_series",
+            "instant": false,
+            "intervalFactor": 1,
+            "legendFormat": "",
+            "refId": "A"
+          }
+        ],
+        "thresholds": "0.9,.99",
+        "title": "SUCCESS RATE",
+        "transparent": true,
+        "type": "singlestat",
+        "valueFontSize": "80%",
+        "valueMaps": [
+          {
+            "op": "=",
+            "text": "N/A",
+            "value": "null"
+          }
+        ],
+        "valueName": "current"
+      },
+      {
+        "cacheTimeout": null,
+        "colorBackground": false,
+        "colorValue": false,
+        "colors": [
+          "#299c46",
+          "rgba(237, 129, 40, 0.89)",
+          "#d44a3a"
+        ],
+        "datasource": "prometheus",
+        "decimals": null,
+        "format": "none",
+        "gauge": {
+          "maxValue": 100,
+          "minValue": 0,
+          "show": false,
+          "thresholdLabels": false,
+          "thresholdMarkers": true
+        },
+        "gridPos": {
+          "h": 4,
+          "w": 8,
+          "x": 8,
+          "y": 2
+        },
+        "id": 6,
+        "interval": null,
+        "links": [],
+        "mappingType": 1,
+        "mappingTypes": [
+          {
+            "name": "value to text",
+            "value": 1
+          },
+          {
+            "name": "range to text",
+            "value": 2
+          }
+        ],
+        "maxDataPoints": 100,
+        "nullPointMode": "connected",
+        "nullText": null,
+        "options": {},
+        "postfix": " RPS",
+        "postfixFontSize": "100%",
+        "prefix": "",
+        "prefixFontSize": "50%",
+        "rangeMaps": [
+          {
+            "from": "null",
+            "text": "N/A",
+            "to": "null"
+          }
+        ],
+        "sparkline": {
+          "fillColor": "rgba(31, 118, 189, 0.18)",
+          "full": true,
+          "lineColor": "rgb(31, 120, 193)",
+          "show": true
+        },
+        "tableColumn": "",
+        "targets": [
+          {
+            "expr": "sum(irate(route_request_total{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s]))",
+            "format": "time_series",
+            "instant": false,
+            "intervalFactor": 1,
+            "legendFormat": "",
+            "refId": "A"
+          }
+        ],
+        "thresholds": "",
+        "title": "REQUEST RATE",
+        "transparent": true,
+        "type": "singlestat",
+        "valueFontSize": "100%",
+        "valueMaps": [
+          {
+            "op": "=",
+            "text": "N/A",
+            "value": "null"
+          }
+        ],
+        "valueName": "current"
+      },
+      {
+        "cacheTimeout": null,
+        "colorBackground": false,
+        "colorValue": false,
+        "colors": [
+          "#299c46",
+          "rgba(237, 129, 40, 0.89)",
+          "#d44a3a"
+        ],
+        "datasource": "prometheus",
+        "decimals": null,
+        "format": "none",
+        "gauge": {
+          "maxValue": 100,
+          "minValue": 0,
+          "show": false,
+          "thresholdLabels": false,
+          "thresholdMarkers": true
+        },
+        "gridPos": {
+          "h": 4,
+          "w": 8,
+          "x": 16,
+          "y": 2
+        },
+        "id": 8,
+        "interval": null,
+        "links": [],
+        "mappingType": 1,
+        "mappingTypes": [
+          {
+            "name": "value to text",
+            "value": 1
+          },
+          {
+            "name": "range to text",
+            "value": 2
+          }
+        ],
+        "maxDataPoints": 100,
+        "nullPointMode": "connected",
+        "nullText": null,
+        "options": {},
+        "postfix": " ms",
+        "postfixFontSize": "100%",
+        "prefix": "",
+        "prefixFontSize": "50%",
+        "rangeMaps": [
+          {
+            "from": "null",
+            "text": "N/A",
+            "to": "null"
+          }
+        ],
+        "sparkline": {
+          "fillColor": "rgba(31, 118, 189, 0.18)",
+          "full": true,
+          "lineColor": "rgb(31, 120, 193)",
+          "show": true
+        },
+        "tableColumn": "",
+        "targets": [
+          {
+            "expr": "histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s])) by (le, rt_route))",
+            "format": "time_series",
+            "instant": false,
+            "intervalFactor": 1,
+            "legendFormat": "",
+            "refId": "A"
+          }
+        ],
+        "thresholds": "",
+        "title": "P95 LATENCY",
+        "transparent": true,
+        "type": "singlestat",
+        "valueFontSize": "100%",
+        "valueMaps": [
+          {
+            "op": "=",
+            "text": "N/A",
+            "value": "null"
+          }
+        ],
+        "valueName": "current"
+      },
+      {
+        "content": "<div class=\"text-center dashboard-header\">\n  <span>TOP-LINE TRAFFIC</span>\n</div>",
+        "gridPos": {
+          "h": 2,
+          "w": 24,
+          "x": 0,
+          "y": 6
+        },
+        "id": 10,
+        "links": [],
+        "mode": "html",
+        "options": {},
+        "title": "",
+        "transparent": true,
+        "type": "text"
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 1,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 0,
+          "y": 8
+        },
+        "id": 12,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(irate(route_response_total{classification=\"success\", namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s])) by (rt_route) / sum(irate(route_response_total{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s])) by (rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "route/{{rt_route}}",
+            "refId": "A"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "SUCCESS RATE",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "decimals": null,
+            "format": "percentunit",
+            "label": "",
+            "logBase": 1,
+            "max": "1",
+            "min": null,
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 0,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 8,
+          "y": 8
+        },
+        "id": 14,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(irate(route_request_total{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\", tls=\"true\"}[30s])) by (rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "🔒route/{{rt_route}}",
+            "refId": "A"
+          },
+          {
+            "expr": "sum(irate(route_request_total{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\", tls!=\"true\"}[30s])) by (rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "route/{{rt_route}}",
+            "refId": "B"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "REQUEST RATE",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "decimals": null,
+            "format": "rps",
+            "label": "",
+            "logBase": 1,
+            "max": null,
+            "min": "0",
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 1,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 16,
+          "y": 8
+        },
+        "id": 16,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "histogram_quantile(0.5, sum(irate(route_response_latency_ms_bucket{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s])) by (le, rt_route))",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "p50 route/{{rt_route}}",
+            "refId": "A"
+          },
+          {
+            "expr": "histogram_quantile(0.95, sum(irate(route_response_latency_ms_bucket{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s])) by (le, rt_route))",
+            "format": "time_series",
+            "hide": false,
+            "intervalFactor": 1,
+            "legendFormat": "p95 route/{{rt_route}}",
+            "refId": "B"
+          },
+          {
+            "expr": "histogram_quantile(0.99, sum(irate(route_response_latency_ms_bucket{namespace=\"$namespace\", direction=\"inbound\", rt_route=\"$rt_route\"}[30s])) by (le, rt_route))",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "p99 route/{{rt_route}}",
+            "refId": "C"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "LATENCY",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "decimals": null,
+            "format": "ms",
+            "label": "",
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "content": "<div class=\"text-center dashboard-header\">\n  <span>INBOUND TRAFFIC BY DEPLOYMENT</span>\n</div>",
+        "gridPos": {
+          "h": 2,
+          "w": 24,
+          "x": 0,
+          "y": 15
+        },
+        "id": 18,
+        "links": [],
+        "mode": "html",
+        "options": {},
+        "title": "",
+        "transparent": true,
+        "type": "text"
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 1,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 0,
+          "y": 17
+        },
+        "id": 20,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(irate(route_response_total{classification=\"success\", namespace=\"$namespace\", direction=\"outbound\", rt_route=\"$rt_route\"}[30s])) by (deployment, rt_route) / sum(irate(route_response_total{namespace=\"$namespace\", direction=\"outbound\", rt_route=\"$rt_route\"}[30s])) by (deployment, rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "deploy/{{deployment}}",
+            "refId": "A"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "SUCCESS RATE",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "decimals": null,
+            "format": "percentunit",
+            "label": "",
+            "logBase": 1,
+            "max": "1",
+            "min": null,
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 0,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 8,
+          "y": 17
+        },
+        "id": 22,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(irate(route_request_total{namespace=\"$namespace\", direction=\"outbound\", tls=\"true\", rt_route=\"$rt_route\"}[30s])) by (deployment, rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "🔒deploy/{{deployment}}",
+            "refId": "A"
+          },
+          {
+            "expr": "sum(irate(route_request_total{namespace=\"$namespace\", direction=\"outbound\", tls!=\"true\", rt_route=\"$rt_route\"}[30s])) by (deployment, rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "deploy/{{deployment}}",
+            "refId": "B"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "REQUEST RATE",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "format": "rps",
+            "label": "",
+            "logBase": 1,
+            "max": null,
+            "min": "0",
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 1,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 16,
+          "y": 17
+        },
+        "id": 24,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "histogram_quantile(0.95, sum(rate(route_response_latency_ms_bucket{namespace=\"$namespace\", direction=\"outbound\", rt_route=\"$rt_route\"}[30s])) by (le, deployment, rt_route))",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "P95 deploy/{{deployment}}",
+            "refId": "A"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "P95 LATENCY",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "format": "ms",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "content": "<div class=\"text-center dashboard-header\">\n  <span>INBOUND TRAFFIC BY POD</span>\n</div>",
+        "gridPos": {
+          "h": 2,
+          "w": 24,
+          "x": 0,
+          "y": 24
+        },
+        "id": 26,
+        "links": [],
+        "mode": "html",
+        "options": {},
+        "title": "",
+        "transparent": true,
+        "type": "text"
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 1,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 0,
+          "y": 26
+        },
+        "id": 28,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(irate(route_response_total{classification=\"success\", namespace=\"$namespace\", direction=\"outbound\", rt_route=\"$rt_route\"}[30s])) by (pod, rt_route) / sum(irate(route_response_total{namespace=\"$namespace\", direction=\"outbound\", rt_route=\"$rt_route\"}[30s])) by (pod, rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "po/{{pod}}",
+            "refId": "A"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "SUCCESS RATE",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "decimals": null,
+            "format": "percentunit",
+            "label": "",
+            "logBase": 1,
+            "max": "1",
+            "min": null,
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 0,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 8,
+          "y": 26
+        },
+        "id": 30,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "sum(irate(route_request_total{namespace=\"$namespace\", direction=\"outbound\", tls=\"true\", rt_route=\"$rt_route\"}[30s])) by (pod, rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "🔒po/{{pod}}",
+            "refId": "A"
+          },
+          {
+            "expr": "sum(irate(route_request_total{namespace=\"$namespace\", direction=\"outbound\", tls!=\"true\", rt_route=\"$rt_route\"}[30s])) by (pod, rt_route)",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "po/{{pod}}",
+            "refId": "B"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "REQUEST RATE",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "format": "rps",
+            "label": "",
+            "logBase": 1,
+            "max": null,
+            "min": "0",
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "aliasColors": {},
+        "bars": false,
+        "dashLength": 10,
+        "dashes": false,
+        "datasource": "prometheus",
+        "fill": 1,
+        "gridPos": {
+          "h": 7,
+          "w": 8,
+          "x": 16,
+          "y": 26
+        },
+        "id": 32,
+        "legend": {
+          "avg": false,
+          "current": false,
+          "max": false,
+          "min": false,
+          "show": false,
+          "total": false,
+          "values": false
+        },
+        "lines": true,
+        "linewidth": 2,
+        "links": [],
+        "nullPointMode": "null",
+        "options": {},
+        "percentage": false,
+        "pointradius": 5,
+        "points": false,
+        "renderer": "flot",
+        "seriesOverrides": [],
+        "spaceLength": 10,
+        "stack": false,
+        "steppedLine": false,
+        "targets": [
+          {
+            "expr": "histogram_quantile(0.95, sum(rate(route_response_latency_ms_bucket{namespace=\"$namespace\", direction=\"outbound\", rt_route=\"$rt_route\"}[30s])) by (le, pod))",
+            "format": "time_series",
+            "intervalFactor": 1,
+            "legendFormat": "P95 po/{{pod, rt_route}}",
+            "refId": "A"
+          }
+        ],
+        "thresholds": [],
+        "timeFrom": null,
+        "timeRegions": [],
+        "timeShift": null,
+        "title": "P95 LATENCY",
+        "tooltip": {
+          "shared": true,
+          "sort": 2,
+          "value_type": "individual"
+        },
+        "type": "graph",
+        "xaxis": {
+          "buckets": null,
+          "mode": "time",
+          "name": null,
+          "show": true,
+          "values": []
+        },
+        "yaxes": [
+          {
+            "format": "ms",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          },
+          {
+            "format": "short",
+            "label": null,
+            "logBase": 1,
+            "max": null,
+            "min": null,
+            "show": true
+          }
+        ],
+        "yaxis": {
+          "align": false,
+          "alignLevel": null
+        }
+      },
+      {
+        "content": "<div>\n  <div style=\"position: absolute; top: 0, left: 0\">\n    <a href=\"https://linkerd.io\" target=\"_blank\"><img src=\"https://linkerd.io/images/identity/svg/linkerd_primary_color_white.svg\" style=\"height: 30px;\"></a>\n  </div>\n  <div id=\"version\" style=\"position: absolute; top: 0; right: 0; font-size: 15px\">\n  </div>\n</div>\n<script type=\"text/javascript\">\nvar localReqURL =\n  window.location.href.substring(\n    0,\n    window.location.href.indexOf(\n    \"/grafana/\"\n    )\n  )+'/overview';\n\nfetch(localReqURL, {\n  credentials: 'include',\n  headers: {\n    \"Content-Type\": \"text/html; charset=utf-8\",\n  },\n})\n.then(response => response.text())\n.then(text => (new window.DOMParser()).parseFromString(text, \"text/html\"))\n.then(html => {\n  var main = html.getElementById('main');\n  var localVersion = main.getAttribute(\"data-release-version\");\n  var versionElem = document.getElementById('version');\n\n  var channel;\n  var parts = localVersion.split(\"-\", 2);\n  if (parts.length === 2) {\n    channel = parts[0];\n    versionElem.innerHTML += 'Running Linkerd ' + parts[1] + ' (' + parts[0] + ')' + '.<br>';\n  } else {\n    versionElem.innerHTML += 'Running Linkerd ' + localVersion + '.<br>';\n  }\n  var uuid = main.getAttribute(\"data-uuid\");\n\n  fetch('https://versioncheck.linkerd.io/version.json?version='+localVersion+'&uuid='+uuid+'&source=grafana', {\n    credentials: 'include',\n    headers: {\n      \"Content-Type\": \"application/json; charset=utf-8\",\n    },\n  })\n  .then(response => response.json())\n  .then(json => {\n    if (!channel || !json[channel]) {\n      versionElem.innerHTML += 'Version check failed.'\n    } else if (json[channel] === localVersion) {\n      versionElem.innerHTML += 'Linkerd is up to date.';\n    } else {\n      parts = json[channel].split(\"-\", 2);\n      if (parts.length === 2) {\n        versionElem.innerHTML += \"A new \"+parts[0]+\" version (\"+parts[1]+\") is available.\"\n      } else {\n        versionElem.innerHTML += \"A new version (\"+json[channel]+\") is available.\"\n      }\n      versionElem.innerHTML += \" <a href='https://versioncheck.linkerd.io/update' target='_blank'>Update now</a>.\";\n    }\n  });\n});\n</script>",
+        "gridPos": {
+          "h": 3,
+          "w": 24,
+          "x": 0,
+          "y": 33
+        },
+        "height": "1px",
+        "id": 34,
+        "links": [],
+        "mode": "html",
+        "options": {},
+        "title": "",
+        "transparent": true,
+        "type": "text"
+      }
+    ],
+    "refresh": "1m",
+    "schemaVersion": 18,
+    "style": "dark",
+    "tags": [
+      "linkerd"
+    ],
+    "templating": {
+      "list": [
+        {
+          "allValue": null,
+          "current": {},
+          "datasource": "prometheus",
+          "definition": "",
+          "hide": 0,
+          "includeAll": false,
+          "label": "Namespace",
+          "multi": false,
+          "name": "namespace",
+          "options": [],
+          "query": "label_values(route_request_total, namespace)",
+          "refresh": 2,
+          "regex": "",
+          "skipUrlSync": false,
+          "sort": 1,
+          "tagValuesQuery": "",
+          "tags": [],
+          "tagsQuery": "",
+          "type": "query",
+          "useTags": false
+        },
+        {
+            "allValue": null,
+            "current": {},
+            "datasource": "prometheus",
+            "definition": "",
+            "hide": 0,
+            "includeAll": false,
+            "label": "Route",
+            "multi": false,
+            "name": "rt_route",
+            "options": [],
+            "query": "label_values(route_request_total{namespace=\"$namespace\"}, rt_route)",
+            "refresh": 2,
+            "regex": "",
+            "skipUrlSync": false,
+            "sort": 1,
+            "tagValuesQuery": "",
+            "tags": [],
+            "tagsQuery": "",
+            "type": "query",
+            "useTags": false
+          }
+      ]
+    },
+    "time": {
+      "from": "now-5m",
+      "to": "now"
+    },
+    "timepicker": {
+      "refresh_intervals": [
+        "5s",
+        "10s",
+        "30s",
+        "1m",
+        "5m",
+        "15m",
+        "30m",
+        "1h",
+        "2h",
+        "1d"
+      ],
+      "time_options": [
+        "5m",
+        "15m",
+        "1h",
+        "6h",
+        "12h",
+        "24h",
+        "2d",
+        "7d",
+        "30d"
+      ]
+    },
+    "timezone": "",
+    "title": "Linkerd Route",
+    "uid": "route",
+    "version": 1
+  }
+  
\ No newline at end of file
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index fa3a54c6b..589e175f7 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -160,11 +160,11 @@ const HintBaseURL = "https://linkerd.io/checks/#"
 
 // AllowedClockSkew sets the allowed skew in clock synchronization
 // between the system running inject command and the node(s), being
-// based on assumed node's heartbeat interval (<= 60 seconds) plus default TLS
+// based on assumed node's heartbeat interval (5 minutes) plus default TLS
 // clock skew allowance.
 //
 // TODO: Make this default value overridiable, e.g. by CLI flag
-const AllowedClockSkew = time.Minute + tls.DefaultClockSkewAllowance
+const AllowedClockSkew = 5*time.Minute + tls.DefaultClockSkewAllowance
 
 var linkerdHAControlPlaneComponents = []string{
 	"linkerd-controller",
@@ -175,20 +175,25 @@ var linkerdHAControlPlaneComponents = []string{
 	"linkerd-tap",
 }
 
+// ExpectedServiceAccountNames is a list of the service accounts that a healthy
+// Linkerd installation should have. Note that linkerd-heartbeat is optional,
+// so it doesn't appear here.
+var ExpectedServiceAccountNames = []string{
+	"linkerd-controller",
+	"linkerd-destination",
+	"linkerd-grafana",
+	"linkerd-identity",
+	"linkerd-prometheus",
+	"linkerd-proxy-injector",
+	"linkerd-smi-metrics",
+	"linkerd-sp-validator",
+	"linkerd-web",
+	"linkerd-tap",
+}
+
 var (
 	retryWindow    = 5 * time.Second
 	requestTimeout = 30 * time.Second
-
-	expectedServiceAccountNames = []string{
-		"linkerd-controller",
-		"linkerd-grafana",
-		"linkerd-identity",
-		"linkerd-prometheus",
-		"linkerd-proxy-injector",
-		"linkerd-sp-validator",
-		"linkerd-web",
-		"linkerd-tap",
-	}
 )
 
 // Resource provides a way to describe a Kubernetes object, kind, and name.
@@ -499,6 +504,7 @@ func (hc *HealthChecker) allCategories() []category {
 				{
 					description: "no clock skew detected",
 					hintAnchor:  "pre-k8s-clock-skew",
+					warning:     true,
 					check: func(context.Context) error {
 						return hc.checkClockSkew()
 					},
@@ -702,7 +708,7 @@ func (hc *HealthChecker) allCategories() []category {
 					hintAnchor:  "l5d-existence-sa",
 					fatal:       true,
 					check: func(context.Context) error {
-						return hc.checkServiceAccounts(expectedServiceAccountNames)
+						return hc.checkServiceAccounts(ExpectedServiceAccountNames)
 					},
 				},
 				{
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index b2e862368..9c7bae54a 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"reflect"
 	"regexp"
 	"sort"
 	"strconv"
@@ -117,6 +118,7 @@ type ResourceConfig struct {
 type patch struct {
 	l5dcharts.Values
 	PathPrefix            string                    `json:"pathPrefix"`
+	AddRootMetadata       bool                      `json:"addRootMetadata"`
 	AddRootAnnotations    bool                      `json:"addRootAnnotations"`
 	Annotations           map[string]string         `json:"annotations"`
 	AddRootLabels         bool                      `json:"addRootLabels"`
@@ -641,6 +643,12 @@ func (conf *ResourceConfig) injectObjectMeta(values *patch) {
 }
 
 func (conf *ResourceConfig) injectPodAnnotations(values *patch) {
+	// ObjectMetaAnnotations.Annotations is nil for new empty structs, but we always initialize
+	// it to an empty map in parse() above, so we follow suit here.
+	emptyMeta := &metav1.ObjectMeta{Annotations: map[string]string{}}
+	// Cronjobs in batch/v1beta1 might have an empty `spec.jobTemplate.spec.template.metadata`
+	// field so we make sure to create it if needed, before attempting adding annotations
+	values.AddRootMetadata = reflect.DeepEqual(conf.pod.meta, emptyMeta)
 	values.AddRootAnnotations = len(conf.pod.meta.Annotations) == 0
 
 	for _, k := range sortedKeys(conf.pod.annotations) {
diff --git a/pkg/inject/report.go b/pkg/inject/report.go
index c7006d407..f8d2994b0 100644
--- a/pkg/inject/report.go
+++ b/pkg/inject/report.go
@@ -10,43 +10,46 @@ import (
 )
 
 const (
-	hostNetworkEnabled               = "host_network_enabled"
-	sidecarExists                    = "sidecar_already_exists"
-	unsupportedResource              = "unsupported_resource"
-	injectEnableAnnotationAbsent     = "injection_enable_annotation_absent"
-	injectDisableAnnotationPresent   = "injection_disable_annotation_present"
-	annotationAtNamespace            = "namespace"
-	annotationAtWorkload             = "workload"
-	invalidInjectAnnotationWorkload  = "invalid_inject_annotation_at_workload"
-	invalidInjectAnnotationNamespace = "invalid_inject_annotation_at_ns"
+	hostNetworkEnabled                   = "host_network_enabled"
+	sidecarExists                        = "sidecar_already_exists"
+	unsupportedResource                  = "unsupported_resource"
+	injectEnableAnnotationAbsent         = "injection_enable_annotation_absent"
+	injectDisableAnnotationPresent       = "injection_disable_annotation_present"
+	annotationAtNamespace                = "namespace"
+	annotationAtWorkload                 = "workload"
+	invalidInjectAnnotationWorkload      = "invalid_inject_annotation_at_workload"
+	invalidInjectAnnotationNamespace     = "invalid_inject_annotation_at_ns"
+	disabledAutomountServiceAccountToken = "disabled_automount_service_account_token_account"
 )
 
 var (
 	// Reasons is a map of inject skip reasons with human readable sentences
 	Reasons = map[string]string{
-		hostNetworkEnabled:               "hostNetwork is enabled",
-		sidecarExists:                    "pod has a sidecar injected already",
-		unsupportedResource:              "this resource kind is unsupported",
-		injectEnableAnnotationAbsent:     fmt.Sprintf("neither the namespace nor the pod have the annotation \"%s:%s\"", k8s.ProxyInjectAnnotation, k8s.ProxyInjectEnabled),
-		injectDisableAnnotationPresent:   fmt.Sprintf("pod has the annotation \"%s:%s\"", k8s.ProxyInjectAnnotation, k8s.ProxyInjectDisabled),
-		invalidInjectAnnotationWorkload:  fmt.Sprintf("invalid value for annotation \"%s\" at workload", k8s.ProxyInjectAnnotation),
-		invalidInjectAnnotationNamespace: fmt.Sprintf("invalid value for annotation \"%s\" at namespace", k8s.ProxyInjectAnnotation),
+		hostNetworkEnabled:                   "hostNetwork is enabled",
+		sidecarExists:                        "pod has a sidecar injected already",
+		unsupportedResource:                  "this resource kind is unsupported",
+		injectEnableAnnotationAbsent:         fmt.Sprintf("neither the namespace nor the pod have the annotation \"%s:%s\"", k8s.ProxyInjectAnnotation, k8s.ProxyInjectEnabled),
+		injectDisableAnnotationPresent:       fmt.Sprintf("pod has the annotation \"%s:%s\"", k8s.ProxyInjectAnnotation, k8s.ProxyInjectDisabled),
+		invalidInjectAnnotationWorkload:      fmt.Sprintf("invalid value for annotation \"%s\" at workload", k8s.ProxyInjectAnnotation),
+		invalidInjectAnnotationNamespace:     fmt.Sprintf("invalid value for annotation \"%s\" at namespace", k8s.ProxyInjectAnnotation),
+		disabledAutomountServiceAccountToken: fmt.Sprintf("automountServiceAccountToken set to \"false\""),
 	}
 )
 
 // Report contains the Kind and Name for a given workload along with booleans
 // describing the result of the injection transformation
 type Report struct {
-	Kind                 string
-	Name                 string
-	HostNetwork          bool
-	Sidecar              bool
-	UDP                  bool // true if any port in any container has `protocol: UDP`
-	UnsupportedResource  bool
-	InjectDisabled       bool
-	InjectDisabledReason string
-	InjectAnnotationAt   string
-	TracingEnabled       bool
+	Kind                         string
+	Name                         string
+	HostNetwork                  bool
+	Sidecar                      bool
+	UDP                          bool // true if any port in any container has `protocol: UDP`
+	UnsupportedResource          bool
+	InjectDisabled               bool
+	InjectDisabledReason         string
+	InjectAnnotationAt           string
+	TracingEnabled               bool
+	AutomountServiceAccountToken bool
 
 	// Uninjected consists of two boolean flags to indicate if a proxy and
 	// proxy-init containers have been uninjected in this report
@@ -73,8 +76,9 @@ func newReport(conf *ResourceConfig) *Report {
 	}
 
 	report := &Report{
-		Kind: strings.ToLower(conf.workload.metaType.Kind),
-		Name: name,
+		Kind:                         strings.ToLower(conf.workload.metaType.Kind),
+		Name:                         name,
+		AutomountServiceAccountToken: true,
 	}
 
 	if conf.pod.meta != nil && conf.pod.spec != nil {
@@ -83,6 +87,9 @@ func newReport(conf *ResourceConfig) *Report {
 		report.Sidecar = healthcheck.HasExistingSidecars(conf.pod.spec)
 		report.UDP = checkUDPPorts(conf.pod.spec)
 		report.TracingEnabled = conf.pod.meta.Annotations[k8s.ProxyTraceCollectorSvcAddrAnnotation] != "" || conf.nsAnnotations[k8s.ProxyTraceCollectorSvcAddrAnnotation] != ""
+		if conf.pod.spec.AutomountServiceAccountToken != nil {
+			report.AutomountServiceAccountToken = *conf.pod.spec.AutomountServiceAccountToken
+		}
 	} else if report.Kind != k8s.Namespace {
 		report.UnsupportedResource = true
 	}
@@ -113,6 +120,10 @@ func (r *Report) Injectable() (bool, []string) {
 		reasons = append(reasons, r.InjectDisabledReason)
 	}
 
+	if !r.AutomountServiceAccountToken {
+		reasons = append(reasons, disabledAutomountServiceAccountToken)
+	}
+
 	if len(reasons) > 0 {
 		return false, reasons
 	}
diff --git a/pkg/k8s/fake.go b/pkg/k8s/fake.go
index 8c6547e94..36591b7b2 100644
--- a/pkg/k8s/fake.go
+++ b/pkg/k8s/fake.go
@@ -115,6 +115,7 @@ func NewFakeClientSets(configs ...string) (
 // newFakeClientSetsFromManifests reads from a slice of readers, each
 // representing a manifest or collection of manifests, and returns a mock
 // Kubernetes ClientSet.
+//nolint:unparam
 func newFakeClientSetsFromManifests(readers []io.Reader) (
 	kubernetes.Interface,
 	apiextensionsclient.Interface,
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index 4a59499bb..92ad276d9 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -377,12 +377,18 @@ const (
 	// RemoteGatewayIdentity follows the same kind of logic as RemoteGatewayNameLabel
 	RemoteGatewayIdentity = SvcMirrorPrefix + "/remote-gateway-identity"
 
+	// GatewayIdentity can be found on the remote gateway service
+	GatewayIdentity = SvcMirrorPrefix + "/gateway-identity"
+
 	// ConfigKeyName is the key in the secret that stores the kubeconfig needed to connect
 	// to a remote cluster
 	ConfigKeyName = "kubeconfig"
 
 	// GatewayPortName is the name of the incoming port of the gateway
 	GatewayPortName = "incoming-port"
+
+	// ServiceMirrorLabel is the value used in the controller component label
+	ServiceMirrorLabel = "servicemirror"
 )
 
 // CreatedByAnnotationValue returns the value associated with
diff --git a/pkg/profiles/proto.go b/pkg/profiles/proto.go
index a36a12eb0..0255b3c83 100644
--- a/pkg/profiles/proto.go
+++ b/pkg/profiles/proto.go
@@ -45,11 +45,18 @@ func protoToServiceProfile(parser *proto.Parser, namespace, name, clusterDomain
 			pkg = typed.Name
 		case *proto.RPC:
 			if service, ok := typed.Parent.(*proto.Service); ok {
+				var path string
+				switch pkg {
+				case "":
+					path = fmt.Sprintf("/%s/%s", service.Name, typed.Name)
+				default:
+					path = fmt.Sprintf("/%s.%s/%s", pkg, service.Name, typed.Name)
+				}
 				route := &sp.RouteSpec{
 					Name: typed.Name,
 					Condition: &sp.RequestMatch{
 						Method:    http.MethodPost,
-						PathRegex: regexp.QuoteMeta(fmt.Sprintf("/%s.%s/%s", pkg, service.Name, typed.Name)),
+						PathRegex: regexp.QuoteMeta(path),
 					},
 				}
 				routes = append(routes, route)
diff --git a/web/Dockerfile b/web/Dockerfile
index c06e9eb4d..d123ba527 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -21,7 +21,7 @@ COPY web/app ./web/app
 RUN ./bin/web build
 
 ## compile go server
-FROM gcr.io/linkerd-io/go-deps:6b9a8ea4 as golang
+FROM gcr.io/linkerd-io/go-deps:1b3f62c3 as golang
 WORKDIR /linkerd-build
 RUN mkdir -p web
 COPY web/main.go web
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index 7137e6dc4..8c5865cc5 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -1596,9 +1596,9 @@ acorn-walk@^6.0.1, acorn-walk@^6.1.1:
   integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
 
 acorn@^5.5.3:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
-  integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
+  version "5.7.4"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
+  integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
 
 acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.1:
   version "6.3.0"


Test patch:
diff --git a/cli/cmd/inject_test.go b/cli/cmd/inject_test.go
index 946c99c9b..9ae7d73e9 100644
--- a/cli/cmd/inject_test.go
+++ b/cli/cmd/inject_test.go
@@ -205,6 +205,20 @@ func TestUninjectAndInject(t *testing.T) {
 			injectProxy:      true,
 			testInjectConfig: defaultConfig,
 		},
+		{
+			inputFileName:    "inject_emojivoto_cronjob.input.yml",
+			goldenFileName:   "inject_emojivoto_cronjob.golden.yml",
+			reportFileName:   "inject_emojivoto_cronjob.report",
+			injectProxy:      false,
+			testInjectConfig: defaultConfig,
+		},
+		{
+			inputFileName:    "inject_emojivoto_cronjob_nometa.input.yml",
+			goldenFileName:   "inject_emojivoto_cronjob_nometa.golden.yml",
+			reportFileName:   "inject_emojivoto_cronjob.report",
+			injectProxy:      false,
+			testInjectConfig: defaultConfig,
+		},
 		{
 			inputFileName:    "inject_emojivoto_pod.input.yml",
 			goldenFileName:   "inject_emojivoto_pod.golden.yml",
@@ -397,6 +411,13 @@ func TestRunInjectCmd(t *testing.T) {
 			exitCode:             0,
 			injectProxy:          true,
 		},
+		{
+			inputFileName:        "inject_emojivoto_deployment_automountServiceAccountToken_false.input.yml",
+			stdOutGoldenFileName: "inject_emojivoto_deployment_automountServiceAccountToken_false.golden.yml",
+			stdErrGoldenFileName: "inject_emojivoto_deployment_automountServiceAccountToken_false.golden.stderr",
+			exitCode:             1,
+			injectProxy:          false,
+		},
 	}
 
 	for i, tc := range testCases {
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob.golden.yml b/cli/cmd/testdata/inject_emojivoto_cronjob.golden.yml
new file mode 100644
index 000000000..3fb2469b6
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob.golden.yml
@@ -0,0 +1,25 @@
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  name: hello
+  namespace: emojivoto
+spec:
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          annotations:
+            linkerd.io/inject: enabled
+          labels:
+            foo: bar
+        spec:
+          containers:
+          - args:
+            - /bin/sh
+            - -c
+            - date; echo Hello from the Kubernetes cluster
+            image: busybox
+            name: hello
+          restartPolicy: OnFailure
+  schedule: '*/10 * * * *'
+---
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob.input.yml b/cli/cmd/testdata/inject_emojivoto_cronjob.input.yml
new file mode 100644
index 000000000..1990e26a1
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob.input.yml
@@ -0,0 +1,22 @@
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  namespace: emojivoto
+  name: hello
+spec:
+  schedule: "*/10 * * * *"
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          labels:
+            foo: bar
+        spec:
+          containers:
+          - name: hello
+            image: busybox
+            args:
+            - /bin/sh
+            - -c
+            - date; echo Hello from the Kubernetes cluster
+          restartPolicy: OnFailure
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob.report b/cli/cmd/testdata/inject_emojivoto_cronjob.report
new file mode 100644
index 000000000..7917ed58d
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob.report
@@ -0,0 +1,3 @@
+
+cronjob "hello" injected
+
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob.report.verbose b/cli/cmd/testdata/inject_emojivoto_cronjob.report.verbose
new file mode 100644
index 000000000..de6f69c5e
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob.report.verbose
@@ -0,0 +1,9 @@
+
+√ pods do not use host networking
+√ pods do not have a 3rd party proxy or initContainer already injected
+√ pods are not annotated to disable injection
+√ at least one resource injected
+√ pod specs do not include UDP ports
+
+cronjob "hello" injected
+
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.golden.yml b/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.golden.yml
new file mode 100644
index 000000000..fd618b72f
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.golden.yml
@@ -0,0 +1,23 @@
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  name: hello
+  namespace: emojivoto
+spec:
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          annotations:
+            linkerd.io/inject: enabled
+        spec:
+          containers:
+          - args:
+            - /bin/sh
+            - -c
+            - date; echo Hello from the Kubernetes cluster
+            image: busybox
+            name: hello
+          restartPolicy: OnFailure
+  schedule: '*/10 * * * *'
+---
diff --git a/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.input.yml b/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.input.yml
new file mode 100644
index 000000000..432691450
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_cronjob_nometa.input.yml
@@ -0,0 +1,19 @@
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  namespace: emojivoto
+  name: hello
+spec:
+  schedule: "*/10 * * * *"
+  jobTemplate:
+    spec:
+      template:
+        spec:
+          containers:
+          - name: hello
+            image: busybox
+            args:
+            - /bin/sh
+            - -c
+            - date; echo Hello from the Kubernetes cluster
+          restartPolicy: OnFailure
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.stderr b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.stderr
new file mode 100644
index 000000000..a1a3ee482
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.stderr
@@ -0,0 +1,1 @@
+Error transforming resources: cannot enable mTLS when automountServiceAccountToken set to "false"
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.stderr.verbose b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.stderr.verbose
new file mode 100644
index 000000000..a1a3ee482
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.stderr.verbose
@@ -0,0 +1,1 @@
+Error transforming resources: cannot enable mTLS when automountServiceAccountToken set to "false"
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.golden.yml
new file mode 100644
index 000000000..e69de29bb
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.input.yml b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.input.yml
new file mode 100644
index 000000000..61614ef8f
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_automountServiceAccountToken_false.input.yml
@@ -0,0 +1,23 @@
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: nginx
+  labels:
+    app: nginx
+spec:
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      automountServiceAccountToken: false
+      containers:
+        - name: nginx
+          image: nginx
+          ports:
+            - name: http
+              containerPort: 80
+---
diff --git a/cli/cmd/testdata/install_config.golden b/cli/cmd/testdata/install_config.golden
index 405002dc2..22156813d 100644
--- a/cli/cmd/testdata/install_config.golden
+++ b/cli/cmd/testdata/install_config.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
diff --git a/cli/cmd/testdata/install_control-plane.golden b/cli/cmd/testdata/install_control-plane.golden
index 4a2de2d3e..84a697581 100644
--- a/cli/cmd/testdata/install_control-plane.golden
+++ b/cli/cmd/testdata/install_control-plane.golden
@@ -2451,7 +2451,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 62731a4e9..11f470b9f 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3394,7 +3397,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index f3c1ff924..1cf5d895e 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3353,7 +3356,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 2d4bdf4f0..8a4828401 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3353,7 +3356,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index ded790e51..a3cf52bb7 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3617,7 +3620,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index ad9476571..3a7f35934 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3617,7 +3620,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 3f97e684e..335100ab0 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -793,6 +793,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3265,7 +3268,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index ec4bf12b1..a94e4796d 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -865,6 +865,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 # Source: linkerd2/templates/smi-metrics-rbac.yaml
 ---
@@ -3220,7 +3223,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index c24363898..14ed94d3b 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -865,6 +865,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 # Source: linkerd2/templates/smi-metrics-rbac.yaml
 ---
@@ -3484,7 +3487,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 2a3bc1239..0e7e4154c 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -834,6 +834,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3053,7 +3056,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index ca6f160a0..cf0a1e2a3 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: Namespace
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: Namespace
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3344,7 +3347,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: ImagePullPolicy
 
         command:
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 133433898..bf5361838 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3353,7 +3356,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_restricted_dashboard.golden b/cli/cmd/testdata/install_restricted_dashboard.golden
index 9bafc11b4..326d1a089 100644
--- a/cli/cmd/testdata/install_restricted_dashboard.golden
+++ b/cli/cmd/testdata/install_restricted_dashboard.golden
@@ -772,6 +772,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3288,7 +3291,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/install_tracing.golden b/cli/cmd/testdata/install_tracing.golden
index 66a63e3cb..6308b20ea 100644
--- a/cli/cmd/testdata/install_tracing.golden
+++ b/cli/cmd/testdata/install_tracing.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3353,7 +3356,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
@@ -3544,17 +3547,11 @@ data:
     tracing:
       enabled: true
       collector:
-        image: omnition/opencensus-collector:0.1.10
+        image: omnition/opencensus-collector:0.1.11
         name: linkerd-collector
-        resources:
-          cpu:
-            limit: 1
-            request: 200m
-          memory:
-            limit: 2Gi
-            request: 400Mi
+        resources: null
       jaeger:
-        image: jaegertracing/all-in-one:1.8
+        image: jaegertracing/all-in-one:1.17.1
         name: linkerd-jaeger
         resources: null
 ---
@@ -3682,7 +3679,7 @@ spec:
         env:
         - name: GOGC
           value: "80"
-        image: omnition/opencensus-collector:0.1.10
+        image: omnition/opencensus-collector:0.1.11
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
@@ -3696,13 +3693,6 @@ spec:
           httpGet:
             path: /
             port: 13133
-        resources:
-          limits:
-            cpu: "1"
-            memory: "2Gi"
-          requests:
-            cpu: "200m"
-            memory: "400Mi"
         volumeMounts:
         - mountPath: /conf
           name: linkerd-collector-config-val
@@ -3898,7 +3888,7 @@ spec:
         linkerd.io/proxy-deployment: linkerd-jaeger
     spec:
       containers:
-      - image: jaegertracing/all-in-one:1.8
+      - image: jaegertracing/all-in-one:1.17.1
         imagePullPolicy: IfNotPresent
         name: jaeger
         ports:
diff --git a/cli/cmd/testdata/upgrade_default.golden b/cli/cmd/testdata/upgrade_default.golden
index ec161c330..182558a11 100644
--- a/cli/cmd/testdata/upgrade_default.golden
+++ b/cli/cmd/testdata/upgrade_default.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3362,7 +3365,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_external_issuer.golden b/cli/cmd/testdata/upgrade_external_issuer.golden
index 60e32fa99..9abe6dc15 100644
--- a/cli/cmd/testdata/upgrade_external_issuer.golden
+++ b/cli/cmd/testdata/upgrade_external_issuer.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3348,7 +3351,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_ha.golden b/cli/cmd/testdata/upgrade_ha.golden
index ca96bdf74..f30911258 100644
--- a/cli/cmd/testdata/upgrade_ha.golden
+++ b/cli/cmd/testdata/upgrade_ha.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3626,7 +3629,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_ha_config.golden b/cli/cmd/testdata/upgrade_ha_config.golden
index 3519f6a10..f0b17d824 100644
--- a/cli/cmd/testdata/upgrade_ha_config.golden
+++ b/cli/cmd/testdata/upgrade_ha_config.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
diff --git a/cli/cmd/testdata/upgrade_overwrite_issuer.golden b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
index 9ef2a4c38..e0d1015e3 100644
--- a/cli/cmd/testdata/upgrade_overwrite_issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3353,7 +3356,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
index bd4bd8cd6..7fdaa6d27 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3339,7 +3342,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
index 9ef2a4c38..e0d1015e3 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
@@ -837,6 +837,9 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-web
   namespace: linkerd
+- kind: ServiceAccount
+  name: linkerd-smi-metrics
+  namespace: linkerd
 ---
 ###
 ### SMI-Metrics RBAC
@@ -3353,7 +3356,7 @@ spec:
       serviceAccountName: linkerd-smi-metrics
       containers:
       - name: shim
-        image: deislabs/smi-metrics:v0.2.0
+        image: deislabs/smi-metrics:v0.2.1
         imagePullPolicy: IfNotPresent
 
         command:
diff --git a/controller/api/destination/endpoint_translator_test.go b/controller/api/destination/endpoint_translator_test.go
index 3f5e22b38..9a07fe75c 100644
--- a/controller/api/destination/endpoint_translator_test.go
+++ b/controller/api/destination/endpoint_translator_test.go
@@ -105,6 +105,13 @@ var (
 		Port:     2,
 		Identity: "some-identity",
 	}
+
+	remoteGatewayWithTLSAndAuthOverride = watcher.Address{
+		IP:                "1.1.1.2",
+		Port:              2,
+		Identity:          "some-identity",
+		AuthorityOverride: "some-auth.com:2",
+	}
 )
 
 func makeEndpointTranslator(t *testing.T) (*mockDestinationGetServer, *endpointTranslator) {
@@ -165,6 +172,46 @@ func TestEndpointTranslatorForRemoteGateways(t *testing.T) {
 		}
 	})
 
+	t.Run("Sends TlsIdentity and Auth override when present", func(t *testing.T) {
+		expectedTLSIdentity := &pb.TlsIdentity_DnsLikeIdentity{
+			Name: "some-identity",
+		}
+
+		expectedProtocolHint := &pb.ProtocolHint{
+			Protocol: &pb.ProtocolHint_H2_{
+				H2: &pb.ProtocolHint_H2{},
+			},
+		}
+
+		expectedAuthOverride := &pb.AuthorityOverride{
+			AuthorityOverride: "some-auth.com:2",
+		}
+
+		mockGetServer, translator := makeEndpointTranslator(t)
+
+		translator.Add(mkAddressSetForServices(remoteGatewayWithTLSAndAuthOverride))
+
+		addrs := mockGetServer.updatesReceived[0].GetAdd().GetAddrs()
+		if len(addrs) != 1 {
+			t.Fatalf("Expected [1] address returned, got %v", addrs)
+		}
+
+		actualTLSIdentity := addrs[0].GetTlsIdentity().GetDnsLikeIdentity()
+		if !reflect.DeepEqual(actualTLSIdentity, expectedTLSIdentity) {
+			t.Fatalf("Expected TlsIdentity to be [%v] but was [%v]", expectedTLSIdentity, actualTLSIdentity)
+		}
+
+		actualProtocolHint := addrs[0].GetProtocolHint()
+		if !reflect.DeepEqual(actualProtocolHint, expectedProtocolHint) {
+			t.Fatalf("Expected ProtoclHint to be [%v] but was [%v]", expectedProtocolHint, actualProtocolHint)
+		}
+
+		actualAuthOverride := addrs[0].GetAuthorityOverride()
+		if !reflect.DeepEqual(actualProtocolHint, expectedProtocolHint) {
+			t.Fatalf("Expected AuthOverride to be [%v] but was [%v]", expectedAuthOverride, actualAuthOverride)
+		}
+	})
+
 	t.Run("Does not send TlsIdentity when not present", func(t *testing.T) {
 		mockGetServer, translator := makeEndpointTranslator(t)
 
diff --git a/controller/api/destination/watcher/endpoints_watcher_test.go b/controller/api/destination/watcher/endpoints_watcher_test.go
index 9e0e09b73..bd4178a89 100644
--- a/controller/api/destination/watcher/endpoints_watcher_test.go
+++ b/controller/api/destination/watcher/endpoints_watcher_test.go
@@ -31,6 +31,9 @@ func addressString(address Address) string {
 	if address.Identity != "" {
 		addressString = fmt.Sprintf("%s/%s", addressString, address.Identity)
 	}
+	if address.AuthorityOverride != "" {
+		addressString = fmt.Sprintf("%s/%s", addressString, address.AuthorityOverride)
+	}
 	return addressString
 }
 
@@ -664,7 +667,7 @@ subsets:
 			id:          ServiceID{Name: "name1-remote", Namespace: "ns"},
 			port:        8989,
 			expectedAddresses: []string{
-				"172.17.0.12:8989/gateway-identity-1",
+				"172.17.0.12:8989/gateway-identity-1/name1-remote-fq:8989",
 			},
 			expectedNoEndpoints:              false,
 			expectedNoEndpointsServiceExists: false,
@@ -700,7 +703,7 @@ subsets:
 			id:          ServiceID{Name: "name1-remote", Namespace: "ns"},
 			port:        8989,
 			expectedAddresses: []string{
-				"172.17.0.12:8989",
+				"172.17.0.12:8989/name1-remote-fq:8989",
 			},
 			expectedNoEndpoints:              false,
 			expectedNoEndpointsServiceExists: false,
@@ -738,7 +741,44 @@ subsets:
 			id:          ServiceID{Name: "name1-remote", Namespace: "ns"},
 			port:        8989,
 			expectedAddresses: []string{
-				"172.17.0.12:9999/gateway-identity-1",
+				"172.17.0.12:9999/gateway-identity-1/name1-remote-fq:8989",
+			},
+			expectedNoEndpoints:              false,
+			expectedNoEndpointsServiceExists: false,
+		},
+		{
+			k8sConfigs: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: name1-remote
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  name: name1-remote
+  namespace: ns
+  annotations:
+    mirror.linkerd.io/remote-gateway-identity: ""
+    mirror.linkerd.io/remote-svc-fq-name: "name1-remote-fq"
+  labels:
+    mirror.linkerd.io/mirrored-service: "true"
+subsets:
+- addresses:
+  - ip: 172.17.0.12
+  ports:
+  - port: 9999`,
+			},
+			serviceType: "mirrored service with empty identity and remapped port in endpoints",
+			id:          ServiceID{Name: "name1-remote", Namespace: "ns"},
+			port:        8989,
+			expectedAddresses: []string{
+				"172.17.0.12:9999/name1-remote-fq:8989",
 			},
 			expectedNoEndpoints:              false,
 			expectedNoEndpointsServiceExists: false,
diff --git a/controller/cmd/service-mirror/cluster_watcher_test.go b/controller/cmd/service-mirror/cluster_watcher_test.go
index eb8980aa1..e2189ed27 100644
--- a/controller/cmd/service-mirror/cluster_watcher_test.go
+++ b/controller/cmd/service-mirror/cluster_watcher_test.go
@@ -145,7 +145,7 @@ func TestRemoteServiceCreated(t *testing.T) {
 				mirroredService("service-one-remote", "ns1", "missing-gateway", "missing-namespace", "111", "", nil),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("service-one-remote", "ns1", "missing-gateway", "missing-namespace", "", nil),
+				endpoints("service-one-remote", "ns1", "missing-gateway", "missing-namespace", "", "", nil),
 			},
 		},
 		{
@@ -188,10 +188,10 @@ func TestRemoteServiceCreated(t *testing.T) {
 					}),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("service-one-remote", "ns1", "existing-gateway", "existing-namespace", "", nil),
+				endpoints("service-one-remote", "ns1", "existing-gateway", "existing-namespace", "", "", nil),
 			},
 			remoteResources: []string{
-				gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "incoming-port-wrong", 888, t),
+				gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "incoming-port-wrong", 888, "", t),
 			},
 		},
 		{
@@ -239,7 +239,7 @@ func TestRemoteServiceCreated(t *testing.T) {
 					}),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("service-one-remote", "ns1", "existing-gateway", "existing-namespace", "192.0.2.127", []corev1.EndpointPort{
+				endpoints("service-one-remote", "ns1", "existing-gateway", "existing-namespace", "192.0.2.127", "gateway-identity", []corev1.EndpointPort{
 					{
 						Name:     "port1",
 						Port:     888,
@@ -253,7 +253,7 @@ func TestRemoteServiceCreated(t *testing.T) {
 				}),
 			},
 			remoteResources: []string{
-				gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "incoming-port", 888, t),
+				gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "incoming-port", 888, "gateway-identity", t),
 			},
 		},
 	} {
@@ -275,7 +275,7 @@ func TestRemoteServiceDeleted(t *testing.T) {
 
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-remote-to-delete-remote", "test-namespace-to-delete", "", "", "", "", nil, t),
-				endpointsAsYaml("test-service-remote-to-delete-remote", "test-namespace-to-delete", "", "", "", nil, t),
+				endpointsAsYaml("test-service-remote-to-delete-remote", "test-namespace-to-delete", "", "", "", "gateway-identity", nil, t),
 			},
 		},
 	} {
@@ -314,7 +314,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 							Port:     222,
 						},
 					}),
-					localEndpoints: endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", []corev1.EndpointPort{
+					localEndpoints: endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", "", []corev1.EndpointPort{
 						{
 							Name:     "port1",
 							Port:     888,
@@ -333,7 +333,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 				},
 			},
 			remoteResources: []string{
-				gatewayAsYaml("gateway-new", "gateway-ns", "currentGatewayResVersion", "0.0.0.0", "incoming-port", 999, t),
+				gatewayAsYaml("gateway-new", "gateway-ns", "currentGatewayResVersion", "0.0.0.0", "incoming-port", 999, "", t),
 			},
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "past", "pastGatewayResVersion", []corev1.ServicePort{
@@ -348,7 +348,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 						Port:     222,
 					},
 				}, t),
-				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", []corev1.EndpointPort{
+				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", "", []corev1.EndpointPort{
 					{
 						Name:     "port1",
 						Port:     888,
@@ -383,7 +383,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 					}),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-remote", "test-namespace", "gateway-new", "gateway-ns", "0.0.0.0", []corev1.EndpointPort{
+				endpoints("test-service-remote", "test-namespace", "gateway-new", "gateway-ns", "0.0.0.0", "", []corev1.EndpointPort{
 					{
 						Name:     "port1",
 						Port:     999,
@@ -425,7 +425,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 							Port:     222,
 						},
 					}),
-					localEndpoints: endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", []corev1.EndpointPort{
+					localEndpoints: endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", "", []corev1.EndpointPort{
 						{
 							Name:     "port1",
 							Port:     888,
@@ -444,7 +444,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 				},
 			},
 			remoteResources: []string{
-				gatewayAsYaml("gateway", "gateway-ns", "currentGatewayResVersion", "192.0.2.127", "incoming-port", 888, t),
+				gatewayAsYaml("gateway", "gateway-ns", "currentGatewayResVersion", "192.0.2.127", "incoming-port", 888, "", t),
 			},
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "past", "pastGatewayResVersion", []corev1.ServicePort{
@@ -464,7 +464,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 						Port:     333,
 					},
 				}, t),
-				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", []corev1.EndpointPort{
+				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", "", []corev1.EndpointPort{
 					{
 						Name:     "port1",
 						Port:     888,
@@ -499,7 +499,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 			},
 
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", []corev1.EndpointPort{
+				endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "192.0.2.127", "", []corev1.EndpointPort{
 					{
 						Name:     "port1",
 						Port:     888,
@@ -572,14 +572,14 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 				}),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0",
+				endpoints("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-1-port",
 							Port:     999,
 							Protocol: "TCP",
 						}}),
-				endpoints("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0",
+				endpoints("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-2-port",
@@ -596,7 +596,7 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 							Port:     8081,
 						},
 					}, t),
-				endpointsAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0",
+				endpointsAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-1-port",
@@ -610,7 +610,7 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 						Port:     8082,
 					},
 				}, t),
-				endpointsAsYaml("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0",
+				endpointsAsYaml("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-2-port",
@@ -668,14 +668,14 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 				}),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.1",
+				endpoints("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.1", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-1-port",
 							Port:     888,
 							Protocol: "TCP",
 						}}),
-				endpoints("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.1",
+				endpoints("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.1", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-2-port",
@@ -693,7 +693,7 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 							Port:     8081,
 						},
 					}, t),
-				endpointsAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0",
+				endpointsAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-1-port",
@@ -707,7 +707,105 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 						Port:     8082,
 					},
 				}, t),
-				endpointsAsYaml("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0",
+				endpointsAsYaml("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
+					[]corev1.EndpointPort{
+						{
+							Name:     "svc-2-port",
+							Port:     888,
+							Protocol: "TCP",
+						}}, t),
+			},
+		},
+
+		{
+			testDescription: "identity is updated on gateway change",
+			events: []interface{}{
+				&RemoteGatewayUpdated{
+					identity:             "new-identity",
+					newPort:              888,
+					newEndpointAddresses: []corev1.EndpointAddress{{IP: "0.0.0.0"}},
+					gatewayData: &gatewayMetadata{
+						Name:      "gateway",
+						Namespace: "gateway-ns",
+					},
+					newResourceVersion: "currentGatewayResVersion",
+					affectedServices: []*corev1.Service{
+						mirroredService("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "pastGatewayResVersion",
+							[]corev1.ServicePort{
+								{
+									Name:     "svc-1-port",
+									Protocol: "TCP",
+									Port:     8081,
+								},
+							}),
+						mirroredService("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "", "pastGatewayResVersion", []corev1.ServicePort{
+							{
+								Name:     "svc-2-port",
+								Protocol: "TCP",
+								Port:     8082,
+							},
+						}),
+					},
+				},
+			},
+			expectedLocalServices: []*corev1.Service{
+				mirroredService("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "currentGatewayResVersion",
+					[]corev1.ServicePort{
+						{
+							Name:     "svc-1-port",
+							Protocol: "TCP",
+							Port:     8081,
+						},
+					}),
+				mirroredService("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "", "currentGatewayResVersion", []corev1.ServicePort{
+					{
+						Name:     "svc-2-port",
+						Protocol: "TCP",
+						Port:     8082,
+					},
+				}),
+			},
+			expectedLocalEndpoints: []*corev1.Endpoints{
+				endpoints("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "new-identity",
+					[]corev1.EndpointPort{
+						{
+							Name:     "svc-1-port",
+							Port:     888,
+							Protocol: "TCP",
+						}}),
+				endpoints("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "new-identity",
+					[]corev1.EndpointPort{
+						{
+							Name:     "svc-2-port",
+							Port:     888,
+							Protocol: "TCP",
+						}}),
+			},
+
+			localResources: []string{
+				mirroredServiceAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "pastGatewayResVersion",
+					[]corev1.ServicePort{
+						{
+							Name:     "svc-1-port",
+							Protocol: "TCP",
+							Port:     8081,
+						},
+					}, t),
+				endpointsAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
+					[]corev1.EndpointPort{
+						{
+							Name:     "svc-1-port",
+							Port:     888,
+							Protocol: "TCP",
+						}}, t),
+				mirroredServiceAsYaml("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "", "pastGatewayResVersion", []corev1.ServicePort{
+					{
+						Name:     "svc-2-port",
+						Protocol: "TCP",
+						Port:     8082,
+					},
+				}, t),
+				endpointsAsYaml("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-2-port",
@@ -752,8 +850,8 @@ func TestRemoteGatewayDeleted(t *testing.T) {
 				}),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", nil),
-				endpoints("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "", nil),
+				endpoints("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "", nil),
+				endpoints("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "", "", nil),
 			},
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "pastGatewayResVersion",
@@ -764,7 +862,7 @@ func TestRemoteGatewayDeleted(t *testing.T) {
 							Port:     8081,
 						},
 					}, t),
-				endpointsAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0",
+				endpointsAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-1-port",
@@ -778,7 +876,7 @@ func TestRemoteGatewayDeleted(t *testing.T) {
 						Port:     8082,
 					},
 				}, t),
-				endpointsAsYaml("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0",
+				endpointsAsYaml("test-service-2-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "",
 					[]corev1.EndpointPort{
 						{
 							Name:     "svc-2-port",
@@ -802,9 +900,9 @@ func TestClusterUnregistered(t *testing.T) {
 			},
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-1-remote", "test-namespace", "", "", "", "", nil, t),
-				endpointsAsYaml("test-service-1-remote", "test-namespace", "", "", "", nil, t),
+				endpointsAsYaml("test-service-1-remote", "test-namespace", "", "", "", "", nil, t),
 				mirroredServiceAsYaml("test-service-2-remote", "test-namespace", "", "", "", "", nil, t),
-				endpointsAsYaml("test-service-2-remote", "test-namespace", "", "", "", nil, t),
+				endpointsAsYaml("test-service-2-remote", "test-namespace", "", "", "", "", nil, t),
 			},
 		},
 	} {
@@ -822,9 +920,9 @@ func TestGcOrphanedServices(t *testing.T) {
 			},
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "", nil, t),
-				endpointsAsYaml("test-service-1-remote", "test-namespace", "", "", "", nil, t),
+				endpointsAsYaml("test-service-1-remote", "test-namespace", "", "", "", "", nil, t),
 				mirroredServiceAsYaml("test-service-2-remote", "test-namespace", "", "", "", "", nil, t),
-				endpointsAsYaml("test-service-2-remote", "test-namespace", "", "", "", nil, t),
+				endpointsAsYaml("test-service-2-remote", "test-namespace", "", "", "", "", nil, t),
 			},
 			remoteResources: []string{
 				remoteServiceAsYaml("test-service-1", "test-namespace", "gateway", "gateway-ns", "", nil, t),
@@ -835,7 +933,7 @@ func TestGcOrphanedServices(t *testing.T) {
 			},
 
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-1-remote", "test-namespace", "", "", "", nil),
+				endpoints("test-service-1-remote", "test-namespace", "", "", "", "", nil),
 			},
 		},
 	} {
@@ -891,11 +989,11 @@ func onAddOrUpdateTestCases(t *testing.T, isAdd bool) []testCase {
 			},
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "pastResourceVersion", "gatewayResVersion", nil, t),
-				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", nil, t),
+				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "", nil, t),
 			},
 			expectedEventsInQueue: []interface{}{&RemoteServiceUpdated{
 				localService:   mirroredService("test-service-remote", "test-namespace", "gateway", "gateway-ns", "pastResourceVersion", "gatewayResVersion", nil),
-				localEndpoints: endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", nil),
+				localEndpoints: endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "", nil),
 				remoteUpdate:   remoteService("test-service", "test-namespace", "gateway", "gateway-ns", "currentResVersion", nil),
 				gatewayData: &gatewayMetadata{
 					Name:      "gateway",
@@ -906,23 +1004,23 @@ func onAddOrUpdateTestCases(t *testing.T, isAdd bool) []testCase {
 				mirroredService("test-service-remote", "test-namespace", "gateway", "gateway-ns", "pastResourceVersion", "gatewayResVersion", nil),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", nil),
+				endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "", nil),
 			},
 		},
 		{
-			testDescription: fmt.Sprintf("not not enqueue any events as this update does not really tell us anything new (res version is the same...) (%s)", testType),
+			testDescription: fmt.Sprintf("not enqueue any events as this update does not really tell us anything new (res version is the same...) (%s)", testType),
 			events: []interface{}{
 				onAddOrUpdateEvent(isAdd, remoteService("test-service", "test-namespace", "gateway", "gateway-ns", "currentResVersion", nil)),
 			},
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "currentResVersion", "gatewayResVersion", nil, t),
-				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", nil, t),
+				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "", nil, t),
 			},
 			expectedLocalServices: []*corev1.Service{
 				mirroredService("test-service-remote", "test-namespace", "gateway", "gateway-ns", "currentResVersion", "gatewayResVersion", nil),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", nil),
+				endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "", nil),
 			},
 		},
 		{
@@ -932,7 +1030,7 @@ func onAddOrUpdateTestCases(t *testing.T, isAdd bool) []testCase {
 			},
 			localResources: []string{
 				mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "currentResVersion", "gatewayResVersion", nil, t),
-				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", nil, t),
+				endpointsAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "", nil, t),
 			},
 			expectedEventsInQueue: []interface{}{&RemoteServiceDeleted{
 				Name:      "test-service",
@@ -943,7 +1041,7 @@ func onAddOrUpdateTestCases(t *testing.T, isAdd bool) []testCase {
 				mirroredService("test-service-remote", "test-namespace", "gateway", "gateway-ns", "currentResVersion", "gatewayResVersion", nil),
 			},
 			expectedLocalEndpoints: []*corev1.Endpoints{
-				endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", nil),
+				endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "", nil),
 			},
 		},
 	}
diff --git a/controller/cmd/service-mirror/cluster_watcher_test_util.go b/controller/cmd/service-mirror/cluster_watcher_test_util.go
index 05d73a4a5..17869dbcb 100644
--- a/controller/cmd/service-mirror/cluster_watcher_test_util.go
+++ b/controller/cmd/service-mirror/cluster_watcher_test_util.go
@@ -131,7 +131,7 @@ func mirroredServiceAsYaml(name, namespace, gtwName, gtwNs, resourceVersion, gat
 	return string(bytes)
 }
 
-func gateway(name, namespace, resourceVersion, ip, portName string, port int32) *corev1.Service {
+func gateway(name, namespace, resourceVersion, ip, portName string, port int32, identity string) *corev1.Service {
 	svc := corev1.Service{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "Service",
@@ -141,6 +141,9 @@ func gateway(name, namespace, resourceVersion, ip, portName string, port int32)
 			Name:            name,
 			Namespace:       namespace,
 			ResourceVersion: resourceVersion,
+			Annotations: map[string]string{
+				consts.GatewayIdentity: identity,
+			},
 		},
 		Spec: corev1.ServiceSpec{
 			Ports: []corev1.ServicePort{
@@ -159,8 +162,8 @@ func gateway(name, namespace, resourceVersion, ip, portName string, port int32)
 	return &svc
 }
 
-func gatewayAsYaml(name, namespace, resourceVersion, ip, portName string, port int32, t *testing.T) string {
-	gtw := gateway(name, namespace, resourceVersion, ip, portName, port)
+func gatewayAsYaml(name, namespace, resourceVersion, ip, portName string, port int32, identity string, t *testing.T) string {
+	gtw := gateway(name, namespace, resourceVersion, ip, portName, port, identity)
 
 	bytes, err := yaml.Marshal(gtw)
 	if err != nil {
@@ -169,7 +172,7 @@ func gatewayAsYaml(name, namespace, resourceVersion, ip, portName string, port i
 	return string(bytes)
 }
 
-func endpoints(name, namespace, gtwName, gtwNs, gatewayIP string, ports []corev1.EndpointPort) *corev1.Endpoints {
+func endpoints(name, namespace, gtwName, gtwNs, gatewayIP string, gatewayIdentity string, ports []corev1.EndpointPort) *corev1.Endpoints {
 	var subsets []corev1.EndpointSubset
 	if gatewayIP != "" {
 		subsets = []corev1.EndpointSubset{
@@ -184,7 +187,7 @@ func endpoints(name, namespace, gtwName, gtwNs, gatewayIP string, ports []corev1
 		}
 	}
 
-	return &corev1.Endpoints{
+	endpoints := &corev1.Endpoints{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "Endpoints",
 			APIVersion: "v1",
@@ -198,13 +201,22 @@ func endpoints(name, namespace, gtwName, gtwNs, gatewayIP string, ports []corev1
 				consts.RemoteGatewayNameLabel: gtwName,
 				consts.RemoteGatewayNsLabel:   gtwNs,
 			},
+			Annotations: map[string]string{
+				consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.cluster.local", strings.Replace(name, "-remote", "", 1), namespace),
+			},
 		},
 		Subsets: subsets,
 	}
+
+	if gatewayIdentity != "" {
+		endpoints.Annotations[consts.RemoteGatewayIdentity] = gatewayIdentity
+	}
+
+	return endpoints
 }
 
-func endpointsAsYaml(name, namespace, gtwName, gtwNs, gatewayIP string, ports []corev1.EndpointPort, t *testing.T) string {
-	ep := endpoints(name, namespace, gtwName, gtwNs, gatewayIP, ports)
+func endpointsAsYaml(name, namespace, gtwName, gtwNs, gatewayIP, gatewayIdentity string, ports []corev1.EndpointPort, t *testing.T) string {
+	ep := endpoints(name, namespace, gtwName, gtwNs, gatewayIP, gatewayIdentity, ports)
 
 	bytes, err := yaml.Marshal(ep)
 	if err != nil {
diff --git a/pkg/charts/linkerd2/values_test.go b/pkg/charts/linkerd2/values_test.go
index 9ca4a649a..11c3f304b 100644
--- a/pkg/charts/linkerd2/values_test.go
+++ b/pkg/charts/linkerd2/values_test.go
@@ -125,7 +125,7 @@ func TestNewValues(t *testing.T) {
 		ProfileValidator: &ProfileValidator{TLS: &TLS{}},
 		Tap:              &Tap{TLS: &TLS{}},
 		SMIMetrics: &SMIMetrics{
-			Image: "deislabs/smi-metrics:v0.2.0",
+			Image: "deislabs/smi-metrics:v0.2.1",
 			TLS:   &TLS{},
 		},
 	}
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index d0f6b54a9..9c45c1e1e 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -749,6 +749,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-destination
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-identity
   namespace: test-ns
@@ -776,6 +785,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-smi-metrics
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-sp-validator
   namespace: test-ns
@@ -942,6 +960,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-destination
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-identity
   namespace: test-ns
@@ -969,6 +996,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-smi-metrics
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-sp-validator
   namespace: test-ns
@@ -1144,6 +1180,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-destination
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-identity
   namespace: test-ns
@@ -1171,6 +1216,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-smi-metrics
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-sp-validator
   namespace: test-ns
@@ -1355,6 +1409,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-destination
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-identity
   namespace: test-ns
@@ -1382,6 +1445,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-smi-metrics
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-sp-validator
   namespace: test-ns
@@ -1575,6 +1647,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-destination
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-identity
   namespace: test-ns
@@ -1602,6 +1683,15 @@ metadata:
 				`
 kind: ServiceAccount
 apiVersion: v1
+metadata:
+  name: linkerd-smi-metrics
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+`,
+				`
+kind: ServiceAccount
+apiVersion: v1
 metadata:
   name: linkerd-sp-validator
   namespace: test-ns
diff --git a/pkg/inject/report_test.go b/pkg/inject/report_test.go
index cf4692605..d59862ec3 100644
--- a/pkg/inject/report_test.go
+++ b/pkg/inject/report_test.go
@@ -276,6 +276,7 @@ func TestDisableByAnnotation(t *testing.T) {
 				resourceConfig := &ResourceConfig{origin: OriginWebhook}
 				resourceConfig.WithNsAnnotations(testCase.nsAnnotations)
 				resourceConfig.pod.meta = testCase.podMeta
+				resourceConfig.pod.spec = &corev1.PodSpec{} //initialize empty spec to prevent test from failing
 
 				report := newReport(resourceConfig)
 				if actual, _, _ := report.disableByAnnotation(resourceConfig); testCase.expected != actual {
@@ -317,6 +318,7 @@ func TestDisableByAnnotation(t *testing.T) {
 			t.Run(fmt.Sprintf("test case #%d", i), func(t *testing.T) {
 				resourceConfig := &ResourceConfig{origin: OriginCLI}
 				resourceConfig.pod.meta = testCase.podMeta
+				resourceConfig.pod.spec = &corev1.PodSpec{} //initialize empty spec to prevent test from failing
 
 				report := newReport(resourceConfig)
 				if actual, _, _ := report.disableByAnnotation(resourceConfig); testCase.expected != actual {
diff --git a/test/install_test.go b/test/install_test.go
index a4fac2037..92dd6024a 100644
--- a/test/install_test.go
+++ b/test/install_test.go
@@ -416,6 +416,24 @@ func TestCheckHelmStableBeforeUpgrade(t *testing.T) {
 	if TestHelper.UpgradeHelmFromVersion() == "" {
 		t.Skip("Skipping as this is not a helm upgrade test")
 	}
+
+	// TODO: remove when 2.8.0 is released
+	_, err := TestHelper.Kubectl("",
+		"--namespace", TestHelper.GetLinkerdNamespace(),
+		"create", "serviceaccount", "linkerd-smi-metrics",
+	)
+	if err != nil {
+		t.Fatalf("linkerd-smi-metrics SA creation failed: %s", err)
+	}
+	_, err = TestHelper.Kubectl("",
+		"--namespace", TestHelper.GetLinkerdNamespace(),
+		"label", "serviceaccount", "linkerd-smi-metrics",
+		"linkerd.io/control-plane-ns="+TestHelper.GetLinkerdNamespace(),
+	)
+	if err != nil {
+		t.Fatalf("linkerd-smi-metrics SA labeling failed: %s", err)
+	}
+
 	testCheckCommand(t, "", TestHelper.UpgradeHelmFromVersion(), "", TestHelper.UpgradeHelmFromVersion())
 }
 
@@ -424,6 +442,16 @@ func TestUpgradeHelm(t *testing.T) {
 		t.Skip("Skipping as this is not a helm upgrade test")
 	}
 
+	// TODO: remove when 2.8.0 is released
+	_, err := TestHelper.Kubectl("",
+		"--namespace", TestHelper.GetLinkerdNamespace(),
+		"delete", "serviceaccount", "linkerd-smi-metrics",
+	)
+	if err != nil {
+		t.Fatalf("linkerd-smi-metrics SA deletion failed: %s", err)
+	}
+	time.Sleep(3 * time.Second)
+
 	args := []string{
 		"--reset-values",
 		"--atomic",
diff --git a/test/serviceaccounts/serviceaccounts_test.go b/test/serviceaccounts/serviceaccounts_test.go
new file mode 100644
index 000000000..e187b66cf
--- /dev/null
+++ b/test/serviceaccounts/serviceaccounts_test.go
@@ -0,0 +1,74 @@
+package serviceaccounts
+
+import (
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/linkerd/linkerd2/pkg/healthcheck"
+	"github.com/linkerd/linkerd2/testutil"
+)
+
+var TestHelper *testutil.TestHelper
+
+func TestMain(m *testing.M) {
+	TestHelper = testutil.NewTestHelper()
+	os.Exit(m.Run())
+}
+
+func namesMatch(names []string) bool {
+	for _, name := range names {
+		if name == "default" || name == "linkerd-heartbeat" {
+			continue
+		}
+		found := false
+		for _, expectedName := range healthcheck.ExpectedServiceAccountNames {
+			if name == expectedName {
+				found = true
+				break
+			}
+		}
+		if !found {
+			return false
+		}
+	}
+	return true
+}
+
+func TestServiceAccountsMatch(t *testing.T) {
+	expectedNames := healthcheck.ExpectedServiceAccountNames
+
+	res, err := TestHelper.Kubectl("",
+		"-n", TestHelper.GetLinkerdNamespace(),
+		"get", "serviceaccounts",
+		"--output", "name",
+	)
+	if err != nil {
+		t.Fatalf("Error retrieving list of service accounts: %s", err)
+	}
+	names := strings.Split(strings.TrimSpace(res), "\n")
+	var saNames []string
+	for _, name := range names {
+		saNames = append(saNames, strings.TrimPrefix(name, "serviceaccount/"))
+	}
+	// disregard `default` and `linkerd-heartbeat`
+	if len(saNames)-2 != len(expectedNames) || !namesMatch(saNames) {
+		t.Fatalf("The service account list doesn't match the expected list: %s", expectedNames)
+	}
+
+	res, err = TestHelper.Kubectl("",
+		"-n", TestHelper.GetLinkerdNamespace(),
+		"get", "rolebindings", "linkerd-psp",
+		"--output", "jsonpath={.subjects[*].name}",
+	)
+	if err != nil {
+		t.Fatalf("Error retrieving list of linkerd-psp rolebindings: %s", err)
+	}
+	saNamesPSP := strings.Split(res, " ")
+	// disregard `linkerd-heartbeat`
+	if len(saNamesPSP)-1 != len(expectedNames) || !namesMatch(saNamesPSP) {
+		t.Fatalf(
+			"The service accounts in the linkerd-psp rolebindings don't match the expected list: %s",
+			expectedNames)
+	}
+}