Repository: linkerd/linkerd2
Pull request #5692 (2021-02-09 01:06:13)

Add EXTENSIONS.md
Extensions specification for Linkerd extensions.

Signed-off-by: Alex Leong <alex@buoyant.io>

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index edd57f4bd..154a66c2c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -105,7 +105,7 @@ jobs:
         - helm-deep
         - helm-upgrade
         - uninstall
-        - upgrade-edge
+        #- upgrade-edge
         - upgrade-stable
         - cni-calico-deep
     needs: [docker_build]
diff --git a/.proxy-version b/.proxy-version
index ce6305098..436c2699d 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.135.0
+v2.136.0
diff --git a/CHANGES.md b/CHANGES.md
index ffc4d68ea..b068ef72a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,29 @@
 # Changes
 
+## edge-21.3.1
+
+This edge release is another release candidate, bringing us closer to
+`stable-2.10.0`! It fixes the Helm install/upgrade procedure and ships some new
+CLI commands, among other improvements.
+
+* Fixed Helm install/upgrade, which was failing when not explicitly setting
+  `proxy.image.version`
+* Added a warning in the dashboard when viewing tap streams from resources that
+  don't have tap enabled
+* Added the command `linkerd viz list` to list meshed pods and indicate which can
+  be tapped, which need to be restarted before they can be tapped, and which
+  have tap disabled
+* Similarly, added the command `linkerd jaeger list` to list meshed pods and
+  indicate which will participate in tracing
+* Added the `--opaque-ports` flag to `linkerd inject` to specify the list of
+  opaque ports when injecting pods (and services)
+* Simplified the output of `linkerd jaeger check`, combining the checks for the
+  status of each component into a single check
+* Changed the destination component to receive the list of default opaque ports
+  set during install so that it's properly reflected during discovery
+* Moved the level of the proxy server's I/O-related "Connection closed" messages
+  from info to debug, which were not providing actionable information
+
 ## edge-21.2.4
 
 This edge is a release candidate for `stable-2.10.0`! It wraps up the functional
diff --git a/EXTENSIONS.md b/EXTENSIONS.md
new file mode 100644
index 000000000..333b04221
--- /dev/null
+++ b/EXTENSIONS.md
@@ -0,0 +1,125 @@
+# Linkerd Extensions
+
+Linkerd has an extension model which allows 3rd parties to add functionality.
+Each extension consists of a binary CLI named `linkerd-name` (where `name` is
+the name of the extension) as well as a set of Kubernetes resources.  To invoke
+an extension, users can run `linkerd name` which will search the current PATH
+for an executable named `linkerd-name` and execute it.
+
+## Installing Extensions
+
+To install an extension, first download the extension executable and put it
+in your PATH. The extension can then be installed into your Kubernetes cluster
+by running `linkerd <extension name> install | kubectl apply -f -`. Similarly,
+it can be uninstalled by running
+`linkerd <extension name> uninstall | kubectl delete -f -`.
+
+A full list of installed extensions can be printed by running `linkerd check`.
+
+## Developing Extensions
+
+The extension must be an executable file named `linkerd-name` where `name` is
+the name of the extension. The name must not be any of the built-in Linkerd
+commands (e.g. `check`) or extensions (e.g. `viz`).
+
+The extension must accept the following flags and respect them any time that
+it communicates with the Kubernetes API.  All of these flags must be accepted
+but may be ignored if they are not applicable.
+
+* `--api-addr`: Override kubeconfig and communicate directly with the control
+  plane at host:port (mostly for testing)
+* `--context`: Name of the kubeconfig context to use
+* `--as`: Username to impersonate for Kubernetes operations
+* `--as-group`: Group to impersonate for Kubernetes operations
+* `--help`/`-h`: Print help message
+* `--kubeconfig`: Path to the kubeconfig file to use for CLI requests
+* `--linkerd-namespace`/`-L`: Namespace in which Linkerd is installed
+  [$LINKERD_NAMESPACE]
+* `--verbose`: Turn on debug logging
+
+The extension must implement these commands:
+
+### `linkerd-name install`
+
+This command must print the Kubernetes manifests for the extension as yaml
+which is suitable to be passed to `kubectl apply -f`. These manifests must
+include a Namespace resource with the label `linkerd.io/extension=name` where
+`name` is the name of the extension. This allows Linkerd to detect installed
+extensions.
+
+### `linkerd-name uninstall`
+
+This command must print manifests for all cluster-scoped Kubernetes resources
+belonging to this extension (including the extension Namespace) as yaml which
+is suitable to be passed to `kubectl delete -f`.
+
+### `linkerd-name check`
+
+This command must perform any appropriate health checks for the extension
+including but not limited to checking that the extension resources exist and
+are in a healthy state. This command must exit with a status code of 0 if the
+checks pass or with a nonzero status code if they do not pass. For consistency,
+it is recommended that this command follows the same output formatting as
+`linkerd check`, e.g.
+
+```console
+linkerd-version
+---------------
+√ can determine the latest version
+√ cli is up-to-date
+
+Status check results are √
+```
+
+The final line of output should be either `Status check results are √` or
+`Status check results are ×`.
+
+In addition to the flags described above, `linkerd-name check` must accept the
+following flags:
+
+* `--namespace`/`-n`: Namespace to use for –proxy checks (default: all
+  namespaces)
+* `--output`/`-o`: Output format. One of: table, json
+* `--pre`: Only run pre-installation checks, to determine if the extension can
+  be installed
+* `--proxy`: Only run data-plane checks, to determine if the data plane is
+  healthy
+* `--wait`: Maximum allowed time for all tests to pass
+
+If the output format is set to json then output must be in json format
+instead of the output format described above.  E.g.
+
+```json
+{
+  "success": false,
+  "categories": [
+    {
+      "categoryName": "kubernetes-api",
+      "checks": [
+        {
+          "description": "can initialize the client",
+          "result": "success"
+        },
+        {
+          "description": "can query the Kubernetes API",
+          "result": "success"
+        },
+        {
+          "description": "linkerd-viz Namespace exists",
+          "hint": "https://linkerd.io/checks/#l5d-viz-ns-exists",
+          "error": "could not find the linkerd-viz extension",
+          "result": "error"
+        }
+      ]
+    }
+  ]
+}
+```
+
+In particular, the `linkerd check` command will invoke the check command for
+each extension installed in the cluster and will request json output.  To
+preserve forwards compatibility, it is recommended that the check command should
+ignore any unknown flags.
+
+The extension may also implement further commands in addition to the ones
+defined here.
diff --git a/charts/linkerd2/templates/controller.yaml b/charts/linkerd2/templates/controller.yaml
index 791989503..1c1ad9abf 100644
--- a/charts/linkerd2/templates/controller.yaml
+++ b/charts/linkerd2/templates/controller.yaml
@@ -72,7 +72,7 @@ spec:
     metadata:
       annotations:
         {{ include "partials.annotations.created-by" . }}
-        {{- include "partials.proxy.annotations" .Values.proxy| nindent 8}}
+        {{- include "partials.proxy.annotations" . | nindent 8}}
         {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }}
       labels:
         linkerd.io/control-plane-component: controller
diff --git a/charts/linkerd2/templates/destination.yaml b/charts/linkerd2/templates/destination.yaml
index 21bec1dcc..37a3709cb 100644
--- a/charts/linkerd2/templates/destination.yaml
+++ b/charts/linkerd2/templates/destination.yaml
@@ -91,7 +91,7 @@ spec:
     metadata:
       annotations:
         {{ include "partials.annotations.created-by" . }}
-        {{- include "partials.proxy.annotations" .Values.proxy| nindent 8}}
+        {{- include "partials.proxy.annotations" . | nindent 8}}
         {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }}
       labels:
         linkerd.io/control-plane-component: destination
@@ -119,6 +119,7 @@ spec:
         - -enable-endpoint-slices={{.Values.enableEndpointSlices}}
         - -cluster-domain={{.Values.clusterDomain}}
         - -identity-trust-domain={{.Values.identityTrustDomain | default .Values.clusterDomain}}
+        - -default-opaque-ports={{.Values.proxy.opaquePorts}}
         {{- include "partials.linkerd.trace" . | nindent 8 -}}
         image: {{.Values.controllerImage}}:{{default .Values.linkerdVersion .Values.controllerImageVersion}}
         imagePullPolicy: {{.Values.imagePullPolicy}}
diff --git a/charts/linkerd2/templates/identity.yaml b/charts/linkerd2/templates/identity.yaml
index 2492efa8e..5578e9f07 100644
--- a/charts/linkerd2/templates/identity.yaml
+++ b/charts/linkerd2/templates/identity.yaml
@@ -110,7 +110,7 @@ spec:
     metadata:
       annotations:
         {{ include "partials.annotations.created-by" . }}
-        {{- include "partials.proxy.annotations" .Values.proxy| nindent 8}}
+        {{- include "partials.proxy.annotations" . | nindent 8}}
         {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }}
       labels:
         linkerd.io/control-plane-component: identity
diff --git a/charts/linkerd2/templates/proxy-injector.yaml b/charts/linkerd2/templates/proxy-injector.yaml
index 0ec351109..26ad212ce 100644
--- a/charts/linkerd2/templates/proxy-injector.yaml
+++ b/charts/linkerd2/templates/proxy-injector.yaml
@@ -36,7 +36,7 @@ spec:
         checksum/config: {{ include (print $.Template.BasePath "/proxy-injector-rbac.yaml") . | sha256sum }}
         {{- end }}
         {{ include "partials.annotations.created-by" . }}
-        {{- include "partials.proxy.annotations" .Values.proxy| nindent 8}}
+        {{- include "partials.proxy.annotations" . | nindent 8}}
         {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }}
       labels:
         linkerd.io/control-plane-component: proxy-injector
diff --git a/charts/linkerd2/templates/sp-validator.yaml b/charts/linkerd2/templates/sp-validator.yaml
index bff04477a..39589c11c 100644
--- a/charts/linkerd2/templates/sp-validator.yaml
+++ b/charts/linkerd2/templates/sp-validator.yaml
@@ -73,7 +73,7 @@ spec:
         checksum/config: {{ include (print $.Template.BasePath "/sp-validator-rbac.yaml") . | sha256sum }}
         {{- end }}
         {{ include "partials.annotations.created-by" . }}
-        {{- include "partials.proxy.annotations" .Values.proxy| nindent 8}}
+        {{- include "partials.proxy.annotations" . | nindent 8}}
         {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }}
       labels:
         linkerd.io/control-plane-component: sp-validator
diff --git a/charts/partials/templates/_metadata.tpl b/charts/partials/templates/_metadata.tpl
index 637afed32..7cf074458 100644
--- a/charts/partials/templates/_metadata.tpl
+++ b/charts/partials/templates/_metadata.tpl
@@ -3,8 +3,8 @@ linkerd.io/created-by: {{ .Values.cliVersion | default (printf "linkerd/helm %s"
 {{- end -}}
 
 {{- define "partials.proxy.annotations" -}}
-linkerd.io/identity-mode: {{ternary "default" "disabled" (not .disableIdentity)}}
-linkerd.io/proxy-version: {{.image.version}}
+linkerd.io/identity-mode: {{ternary "default" "disabled" (not .Values.proxy.disableIdentity)}}
+linkerd.io/proxy-version: {{.Values.proxy.image.version | default .Values.linkerdVersion}}
 {{- end -}}
 
 {{/*
diff --git a/cli/cmd/endpoints.go b/cli/cmd/endpoints.go
index 19714357a..7b405dbf6 100644
--- a/cli/cmd/endpoints.go
+++ b/cli/cmd/endpoints.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 	"sync"
 	"text/tabwriter"
+	"time"
 
 	destinationPb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	netPb "github.com/linkerd/linkerd2-proxy-api/go/net"
@@ -89,13 +90,14 @@ destination.`,
 				return err
 			}
 
-			endpoints, err := requestEndpointsFromAPI(api.CheckPublicAPIClientOrExit(healthcheck.Options{
+			endpoints, err := requestEndpointsFromAPI(api.CheckPublicAPIClientOrRetryOrExit(healthcheck.Options{
 				ControlPlaneNamespace: controlPlaneNamespace,
 				KubeConfig:            kubeconfigPath,
 				Impersonate:           impersonate,
 				ImpersonateGroup:      impersonateGroup,
 				KubeContext:           kubeContext,
 				APIAddr:               apiAddr,
+				RetryDeadline:         time.Time{},
 			}), args)
 			if err != nil {
 				fmt.Fprint(os.Stderr, fmt.Errorf("Destination API error: %s", err))
diff --git a/cli/cmd/inject.go b/cli/cmd/inject.go
index b170c476f..42112bc19 100644
--- a/cli/cmd/inject.go
+++ b/cli/cmd/inject.go
@@ -169,9 +169,14 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
 	}
 
 	reports := []inject.Report{*report}
-	// Injection of services depends on being able to retrieve the namespace
-	// annotations which can only occur in the proxy injector webhook.
+
 	if conf.IsService() {
+		opaquePortsAnnotations := map[string]string{}
+		if opaquePorts, ok := rt.overrideAnnotations[k8s.ProxyOpaquePortsAnnotation]; ok {
+			opaquePortsAnnotations[k8s.ProxyOpaquePortsAnnotation] = opaquePorts
+			b, err := conf.AnnotateService(opaquePortsAnnotations)
+			return b, reports, err
+		}
 		return bytes, reports, nil
 	}
 
@@ -204,6 +209,7 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
 	if err != nil {
 		return nil, nil, err
 	}
+
 	if len(patchJSON) == 0 {
 		return bytes, reports, nil
 	}
@@ -323,9 +329,7 @@ func (resourceTransformerInject) generateReport(reports []inject.Report, output
 	}
 
 	for _, r := range reports {
-		if r.Kind == k8s.Service {
-			output.Write([]byte(fmt.Sprintf("service \"%s\" skipped\n", r.Name)))
-		} else if b, _ := r.Injectable(); b {
+		if b, _ := r.Injectable(); b {
 			output.Write([]byte(fmt.Sprintf("%s \"%s\" injected\n", r.Kind, r.Name)))
 		} else {
 			if r.Kind != "" {
@@ -342,13 +346,14 @@ func (resourceTransformerInject) generateReport(reports []inject.Report, output
 
 func fetchConfigs(ctx context.Context) (*linkerd2.Values, error) {
 
-	api.CheckPublicAPIClientOrExit(healthcheck.Options{
+	api.CheckPublicAPIClientOrRetryOrExit(healthcheck.Options{
 		ControlPlaneNamespace: controlPlaneNamespace,
 		KubeConfig:            kubeconfigPath,
 		Impersonate:           impersonate,
 		ImpersonateGroup:      impersonateGroup,
 		KubeContext:           kubeContext,
 		APIAddr:               apiAddr,
+		RetryDeadline:         time.Time{},
 	})
 
 	api, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
@@ -392,6 +397,9 @@ func getOverrideAnnotations(values *charts.Values, base *charts.Values) map[stri
 	if proxy.Ports.Outbound != baseProxy.Ports.Outbound {
 		overrideAnnotations[k8s.ProxyOutboundPortAnnotation] = fmt.Sprintf("%d", proxy.Ports.Outbound)
 	}
+	if proxy.OpaquePorts != baseProxy.OpaquePorts {
+		overrideAnnotations[k8s.ProxyOpaquePortsAnnotation] = proxy.OpaquePorts
+	}
 
 	if proxy.Image.Name != baseProxy.Image.Name {
 		overrideAnnotations[k8s.ProxyImageAnnotation] = proxy.Image.Name
diff --git a/cli/cmd/options.go b/cli/cmd/options.go
index 0db8b3b4c..cf820754d 100644
--- a/cli/cmd/options.go
+++ b/cli/cmd/options.go
@@ -471,6 +471,12 @@ func makeInjectFlags(defaults *l5dcharts.Values) ([]flag.Flag, *pflag.FlagSet) {
 				values.Proxy.IsIngress = value
 				return nil
 			}),
+
+		flag.NewStringSliceFlag(injectFlags, "opaque-ports", strings.Split(defaults.Proxy.OpaquePorts, ","),
+			"Set opaque ports on the proxy", func(values *l5dcharts.Values, value []string) error {
+				values.Proxy.OpaquePorts = strings.Join(value, ",")
+				return nil
+			}),
 	}
 
 	return flags, injectFlags
diff --git a/cli/cmd/uninject.go b/cli/cmd/uninject.go
index 07b49464c..7be001540 100644
--- a/cli/cmd/uninject.go
+++ b/cli/cmd/uninject.go
@@ -78,6 +78,7 @@ func (rt resourceTransformerUninject) transform(bytes []byte) ([]byte, []inject.
 		output = bytes
 		report.UnsupportedResource = true
 	}
+
 	return output, []inject.Report{*report}, nil
 }
 
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index 3a849da1d..3fb4b6648 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -10,7 +10,6 @@ import (
 	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
 	"github.com/linkerd/linkerd2/pkg/addr"
 	"github.com/linkerd/linkerd2/pkg/k8s"
-	"github.com/linkerd/linkerd2/pkg/opaqueports"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
 	coreinformers "k8s.io/client-go/informers/core/v1"
@@ -30,6 +29,7 @@ type endpointTranslator struct {
 	identityTrustDomain string
 	enableH2Upgrade     bool
 	nodeTopologyLabels  map[string]string
+	defaultOpaquePorts  map[uint32]struct{}
 
 	availableEndpoints watcher.AddressSet
 	filteredSnapshot   watcher.AddressSet
@@ -43,6 +43,7 @@ func newEndpointTranslator(
 	enableH2Upgrade bool,
 	service string,
 	srcNodeName string,
+	defaultOpaquePorts map[uint32]struct{},
 	nodes coreinformers.NodeInformer,
 	stream pb.Destination_GetServer,
 	log *logging.Entry,
@@ -65,6 +66,7 @@ func newEndpointTranslator(
 		identityTrustDomain,
 		enableH2Upgrade,
 		nodeTopologyLabels,
+		defaultOpaquePorts,
 		availableEndpoints,
 		filteredSnapshot,
 		stream,
@@ -221,7 +223,7 @@ func (et *endpointTranslator) sendClientAdd(set watcher.AddressSet) {
 			// If the opaque ports annotation was not set, then set the
 			// endpoint's opaque ports to the default value.
 			if !ok {
-				opaquePorts = opaqueports.DefaultOpaquePorts
+				opaquePorts = et.defaultOpaquePorts
 			}
 			wa, err = toWeightedAddr(address, opaquePorts, et.enableH2Upgrade, et.identityTrustDomain, et.controllerNS, et.log)
 		} else {
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index 34ab39d77..da0e8b50c 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -12,7 +12,6 @@ import (
 	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	labels "github.com/linkerd/linkerd2/pkg/k8s"
-	"github.com/linkerd/linkerd2/pkg/opaqueports"
 	"github.com/linkerd/linkerd2/pkg/prometheus"
 	"github.com/linkerd/linkerd2/pkg/util"
 	logging "github.com/sirupsen/logrus"
@@ -37,6 +36,7 @@ type (
 		controllerNS        string
 		identityTrustDomain string
 		clusterDomain       string
+		defaultOpaquePorts  map[uint32]struct{}
 
 		k8sAPI   *k8s.API
 		log      *logging.Entry
@@ -64,14 +64,16 @@ func NewServer(
 	enableEndpointSlices bool,
 	k8sAPI *k8s.API,
 	clusterDomain string,
+	defaultOpaquePorts map[uint32]struct{},
 	shutdown <-chan struct{},
 ) *grpc.Server {
 	log := logging.WithFields(logging.Fields{
 		"addr":      addr,
 		"component": "server",
 	})
+
 	endpoints := watcher.NewEndpointsWatcher(k8sAPI, log, enableEndpointSlices)
-	opaquePorts := watcher.NewOpaquePortsWatcher(k8sAPI, log)
+	opaquePorts := watcher.NewOpaquePortsWatcher(k8sAPI, log, defaultOpaquePorts)
 	profiles := watcher.NewProfileWatcher(k8sAPI, log)
 	trafficSplits := watcher.NewTrafficSplitWatcher(k8sAPI, log)
 	ips := watcher.NewIPWatcher(k8sAPI, endpoints, log)
@@ -87,6 +89,7 @@ func NewServer(
 		controllerNS,
 		identityTrustDomain,
 		clusterDomain,
+		defaultOpaquePorts,
 		k8sAPI,
 		log,
 		shutdown,
@@ -118,6 +121,7 @@ func (s *server) Get(dest *pb.GetDestination, stream pb.Destination_GetServer) e
 		s.enableH2Upgrade,
 		dest.GetPath(),
 		token.NodeName,
+		s.defaultOpaquePorts,
 		s.nodes,
 		stream,
 		log,
@@ -225,7 +229,7 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 				// If the opaque ports annotation was not set, then set the
 				// endpoint's opaque ports to the default value.
 				if !ok {
-					opaquePorts = opaqueports.DefaultOpaquePorts
+					opaquePorts = s.defaultOpaquePorts
 				}
 				endpoint, err = toWeightedAddr(podSet.Addresses[podID], opaquePorts, s.enableH2Upgrade, s.identityTrustDomain, s.controllerNS, log)
 				if err != nil {
diff --git a/controller/api/destination/watcher/opaque_ports_watcher.go b/controller/api/destination/watcher/opaque_ports_watcher.go
index 33a45304a..02b828085 100644
--- a/controller/api/destination/watcher/opaque_ports_watcher.go
+++ b/controller/api/destination/watcher/opaque_ports_watcher.go
@@ -7,7 +7,6 @@ import (
 	"github.com/linkerd/linkerd2-proxy-init/ports"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	labels "github.com/linkerd/linkerd2/pkg/k8s"
-	"github.com/linkerd/linkerd2/pkg/opaqueports"
 	"github.com/linkerd/linkerd2/pkg/util"
 	log "github.com/sirupsen/logrus"
 	logging "github.com/sirupsen/logrus"
@@ -20,9 +19,10 @@ type (
 	// opaque ports annotation is added to a service, the watcher will update
 	// listeners—if any—subscribed to that service.
 	OpaquePortsWatcher struct {
-		subscriptions map[ServiceID]*svcSubscriptions
-		k8sAPI        *k8s.API
-		log           *logging.Entry
+		subscriptions      map[ServiceID]*svcSubscriptions
+		k8sAPI             *k8s.API
+		log                *logging.Entry
+		defaultOpaquePorts map[uint32]struct{}
 		sync.RWMutex
 	}
 
@@ -39,11 +39,12 @@ type (
 
 // NewOpaquePortsWatcher creates a OpaquePortsWatcher and begins watching for
 // k8sAPI for service changes.
-func NewOpaquePortsWatcher(k8sAPI *k8s.API, log *logging.Entry) *OpaquePortsWatcher {
+func NewOpaquePortsWatcher(k8sAPI *k8s.API, log *logging.Entry, opaquePorts map[uint32]struct{}) *OpaquePortsWatcher {
 	opw := &OpaquePortsWatcher{
-		subscriptions: make(map[ServiceID]*svcSubscriptions),
-		k8sAPI:        k8sAPI,
-		log:           log.WithField("component", "opaque-ports-watcher"),
+		subscriptions:      make(map[ServiceID]*svcSubscriptions),
+		k8sAPI:             k8sAPI,
+		log:                log.WithField("component", "opaque-ports-watcher"),
+		defaultOpaquePorts: opaquePorts,
 	}
 	k8sAPI.Svc().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 		AddFunc:    opw.addService,
@@ -69,7 +70,7 @@ func (opw *OpaquePortsWatcher) Subscribe(id ServiceID, listener OpaquePortsUpdat
 	// and no opaque ports
 	if !ok {
 		opw.subscriptions[id] = &svcSubscriptions{
-			opaquePorts: opaqueports.DefaultOpaquePorts,
+			opaquePorts: opw.defaultOpaquePorts,
 			listeners:   []OpaquePortsUpdateListener{listener},
 		}
 		return nil
@@ -121,7 +122,7 @@ func (opw *OpaquePortsWatcher) addService(obj interface{}) {
 	// If the opaque ports annotation was not set, then set the service's
 	// opaque ports to the default value.
 	if !ok {
-		opaquePorts = opaqueports.DefaultOpaquePorts
+		opaquePorts = opw.defaultOpaquePorts
 	}
 	ss, ok := opw.subscriptions[id]
 	// If there are no subscriptions for this service, create one with the
@@ -172,7 +173,7 @@ func (opw *OpaquePortsWatcher) deleteService(obj interface{}) {
 		return
 	}
 	old := ss.opaquePorts
-	ss.opaquePorts = opaqueports.DefaultOpaquePorts
+	ss.opaquePorts = opw.defaultOpaquePorts
 	// Do not send an update if the service already had the default opaque ports
 	if portsEqual(old, ss.opaquePorts) {
 		return
diff --git a/controller/cmd/destination/main.go b/controller/cmd/destination/main.go
index 02ff3ae9f..197d33886 100644
--- a/controller/cmd/destination/main.go
+++ b/controller/cmd/destination/main.go
@@ -14,6 +14,7 @@ import (
 	"github.com/linkerd/linkerd2/pkg/flags"
 	pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/trace"
+	"github.com/linkerd/linkerd2/pkg/util"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -30,6 +31,8 @@ func Main(args []string) {
 	enableEndpointSlices := cmd.Bool("enable-endpoint-slices", false, "Enable the usage of EndpointSlice informers and resources")
 	trustDomain := cmd.String("identity-trust-domain", "", "configures the name suffix used for identities")
 	clusterDomain := cmd.String("cluster-domain", "", "kubernetes cluster domain")
+	defaultOpaquePorts := cmd.String("default-opaque-ports", "", "configures the default opaque ports")
+
 	traceCollector := flags.AddTraceFlags(cmd)
 
 	flags.ConfigureAndParse(cmd, args)
@@ -58,6 +61,13 @@ func Main(args []string) {
 		log.Warnf("expected cluster domain through args (falling back to %s)", *clusterDomain)
 	}
 
+	opaquePorts, err := util.ParsePorts(*defaultOpaquePorts)
+	if err != nil {
+		log.Fatalf("Failed to parse opaque Ports %s: %s", *defaultOpaquePorts, err)
+	}
+
+	log.Infof("Using default opaque ports: %v", opaquePorts)
+
 	if *traceCollector != "" {
 		if err := trace.InitializeTracing("linkerd-destination", *traceCollector); err != nil {
 			log.Warnf("failed to initialize tracing: %s", err)
@@ -106,6 +116,7 @@ func Main(args []string) {
 		*enableEndpointSlices,
 		k8sAPI,
 		*clusterDomain,
+		opaquePorts,
 		done,
 	)
 
diff --git a/jaeger/cmd/check.go b/jaeger/cmd/check.go
index d208ff641..261e4cb26 100644
--- a/jaeger/cmd/check.go
+++ b/jaeger/cmd/check.go
@@ -5,13 +5,10 @@ import (
 	"fmt"
 	"io"
 	"os"
-	"strings"
 	"time"
 
-	"github.com/linkerd/linkerd2/jaeger/pkg/labels"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -22,9 +19,6 @@ const (
 
 	// linkerdJaegerExtensionCheck adds checks related to the jaeger extension
 	linkerdJaegerExtensionCheck healthcheck.CategoryID = "linkerd-jaeger"
-
-	// linkerdJaegerExtensionDataPlaneCheck adds checks related to the jaeger extension
-	linkerdJaegerExtensionDataPlaneCheck healthcheck.CategoryID = JaegerExtensionName + "-data-plane"
 )
 
 var (
@@ -32,10 +26,8 @@ var (
 )
 
 type checkOptions struct {
-	wait      time.Duration
-	output    string
-	proxy     bool
-	namespace string
+	wait   time.Duration
+	output string
 }
 
 func jaegerCategory(hc *healthcheck.HealthChecker) *healthcheck.Category {
@@ -93,100 +85,29 @@ func jaegerCategory(hc *healthcheck.HealthChecker) *healthcheck.Category {
 			}))
 
 	checkers = append(checkers,
-		*healthcheck.NewChecker("collector pod is running").
-			WithHintAnchor("l5d-jaeger-collector-running").
+		*healthcheck.NewChecker("jaeger extension pods are running").
+			WithHintAnchor("l5d-jaeger-pods-running").
 			Fatal().
 			WithRetryDeadline(hc.RetryDeadline).
 			SurfaceErrorOnRetry().
 			WithCheck(func(ctx context.Context) error {
-				// Check for Collector pod
-				podList, err := hc.KubeAPIClient().CoreV1().Pods(jaegerNamespace).List(ctx, metav1.ListOptions{LabelSelector: "component=collector"})
+				pods, err := hc.KubeAPIClient().GetPodsByNamespace(ctx, jaegerNamespace)
 				if err != nil {
 					return err
 				}
-				return healthcheck.CheckPodsRunning(podList.Items, fmt.Sprintf("No collector pods found in the %s namespace", jaegerNamespace))
-			}))
 
-	checkers = append(checkers,
-		*healthcheck.NewChecker("jaeger pod is running").
-			WithHintAnchor("l5d-jaeger-jaeger-running").
-			Fatal().
-			WithRetryDeadline(hc.RetryDeadline).
-			SurfaceErrorOnRetry().
-			WithCheck(func(ctx context.Context) error {
-				// Check for Jaeger pod
-				podList, err := hc.KubeAPIClient().CoreV1().Pods(jaegerNamespace).List(ctx, metav1.ListOptions{LabelSelector: "component=jaeger"})
+				// Check for relevant pods to be present
+				err = healthcheck.CheckForPods(pods, []string{"collector", "jaeger", "jaeger-injector"})
 				if err != nil {
 					return err
 				}
-				return healthcheck.CheckPodsRunning(podList.Items, fmt.Sprintf("No jaeger pods found in the %s namespace", jaegerNamespace))
-			}))
 
-	checkers = append(checkers,
-		*healthcheck.NewChecker("jaeger injector pod is running").
-			WithHintAnchor("l5d-jaeger-jaeger-running").
-			Fatal().
-			WithRetryDeadline(hc.RetryDeadline).
-			SurfaceErrorOnRetry().
-			WithCheck(func(ctx context.Context) error {
-				// Check for Jaeger Injector pod
-				podList, err := hc.KubeAPIClient().CoreV1().Pods(jaegerNamespace).List(ctx, metav1.ListOptions{LabelSelector: "component=jaeger-injector"})
-				if err != nil {
-					return err
-				}
-				return healthcheck.CheckPodsRunning(podList.Items, fmt.Sprintf("No jaeger injector pods found in the %s namespace", jaegerNamespace))
+				return healthcheck.CheckPodsRunning(pods, "")
 			}))
 
 	return healthcheck.NewCategory(linkerdJaegerExtensionCheck, checkers, true)
 }
 
-// JaegerDataPlaneCategory returns a healthcheck.Category containing checkers
-// to verify the jaeger injection
-func JaegerDataPlaneCategory(hc *healthcheck.HealthChecker) *healthcheck.Category {
-	return healthcheck.NewCategory(linkerdJaegerExtensionDataPlaneCheck, []healthcheck.Checker{
-		*healthcheck.NewChecker("data plane namespace exists").
-			WithHintAnchor("l5d-data-plane-exists").
-			Fatal().
-			WithCheck(func(ctx context.Context) error {
-				if hc.DataPlaneNamespace == "" {
-					// when checking proxies in all namespaces, this check is a no-op
-					return nil
-				}
-				return hc.CheckNamespace(ctx, hc.DataPlaneNamespace, true)
-			}),
-		*healthcheck.NewChecker("data-plane pods have tracing enabled").
-			WithHintAnchor("l5d-jaeger-data-plane-trace").
-			Warning().
-			WithCheck(func(ctx context.Context) error {
-				pods, err := hc.KubeAPIClient().GetPodsByNamespace(ctx, hc.DataPlaneNamespace)
-				if err != nil {
-					return err
-				}
-
-				return checkForTraceConfiguration(pods)
-			}),
-	}, true)
-}
-
-// checkForTraceConfiguration checks if the trace annotation is present
-// only for the pods with tracing enabled
-func checkForTraceConfiguration(pods []corev1.Pod) error {
-	var podsWithoutTraceConfig []string
-	for i := range pods {
-		pod := pods[i]
-		// Check for jaeger-injector annotation
-		if !labels.IsTracingEnabled(&pod) {
-			podsWithoutTraceConfig = append(podsWithoutTraceConfig, fmt.Sprintf("* %s/%s", pod.Namespace, pod.Name))
-		}
-
-	}
-
-	if len(podsWithoutTraceConfig) > 0 {
-		return fmt.Errorf("Some data plane pods do not have tracing configured and cannot be traced:\n\t%s", strings.Join(podsWithoutTraceConfig, "\n\t"))
-	}
-	return nil
-}
-
 func newCheckOptions() *checkOptions {
 	return &checkOptions{
 		wait:   300 * time.Second,
@@ -223,8 +144,10 @@ code.`,
 
 	cmd.Flags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: basic, json")
 	cmd.Flags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
-	cmd.Flags().BoolVar(&options.proxy, "proxy", options.proxy, "Also run data-plane checks, to determine if the data plane is healthy")
-	cmd.Flags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace to use for --proxy checks (default: all namespaces)")
+	cmd.Flags().Bool("proxy", false, "Also run data-plane checks, to determine if the data plane is healthy")
+	cmd.Flags().StringP("namespace", "n", "", "Namespace to use for --proxy checks (default: all namespaces)")
+	cmd.Flags().MarkHidden("proxy")
+	cmd.Flags().MarkHidden("namespace")
 
 	return cmd
 }
@@ -243,7 +166,6 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
 		ImpersonateGroup:      impersonateGroup,
 		APIAddr:               apiAddr,
 		RetryDeadline:         time.Now().Add(options.wait),
-		DataPlaneNamespace:    options.namespace,
 	})
 
 	err = hc.InitializeKubeAPIClient()
@@ -254,9 +176,6 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
 	}
 
 	hc.AppendCategories(*jaegerCategory(hc))
-	if options.proxy {
-		hc.AppendCategories(*JaegerDataPlaneCategory(hc))
-	}
 
 	success := healthcheck.RunChecks(wout, werr, hc, options.output)
 
diff --git a/jaeger/cmd/install.go b/jaeger/cmd/install.go
index c9f766017..c453f45d6 100644
--- a/jaeger/cmd/install.go
+++ b/jaeger/cmd/install.go
@@ -58,7 +58,7 @@ A full list of configurable values can be found at https://www.github.com/linker
 					ImpersonateGroup:      impersonateGroup,
 					APIAddr:               apiAddr,
 					RetryDeadline:         time.Now().Add(wait),
-				}, true)
+				})
 			}
 
 			return install(os.Stdout, options)
diff --git a/jaeger/cmd/list.go b/jaeger/cmd/list.go
new file mode 100644
index 000000000..1f2d964a6
--- /dev/null
+++ b/jaeger/cmd/list.go
@@ -0,0 +1,85 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/linkerd/linkerd2/jaeger/pkg/labels"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/spf13/cobra"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type listOptions struct {
+	namespace     string
+	allNamespaces bool
+}
+
+func newCmdList() *cobra.Command {
+	var options listOptions
+
+	cmd := &cobra.Command{
+		Use:   "list [flags]",
+		Short: "Lists which pods have tracing enabled",
+		Args:  cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return err
+			}
+
+			if options.namespace == "" {
+				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
+			}
+			if options.allNamespaces {
+				options.namespace = v1.NamespaceAll
+			}
+
+			pods, err := k8sAPI.CoreV1().Pods(options.namespace).List(cmd.Context(), metav1.ListOptions{})
+			if err != nil {
+				fmt.Fprintln(os.Stderr, err)
+				os.Exit(1)
+			}
+
+			var tracingEnabled, tracingNotEnabled []v1.Pod
+
+			for _, pod := range pods.Items {
+				pod := pod
+				if pkgK8s.IsMeshed(&pod, controlPlaneNamespace) {
+					if labels.IsTracingEnabled(&pod) {
+						tracingEnabled = append(tracingEnabled, pod)
+					} else {
+						tracingNotEnabled = append(tracingNotEnabled, pod)
+					}
+				}
+			}
+
+			if len(tracingEnabled) > 0 {
+				fmt.Println("Pods with tracing enabled:")
+				for _, pod := range tracingEnabled {
+					fmt.Printf("\t* %s/%s\n", pod.Namespace, pod.Name)
+				}
+			}
+
+			if len(tracingNotEnabled) > 0 {
+				fmt.Println("Pods missing tracing configuration (restart these pods to enable tracing):")
+				for _, pod := range tracingNotEnabled {
+					fmt.Printf("\t* %s/%s\n", pod.Namespace, pod.Name)
+				}
+			}
+
+			if len(tracingEnabled)+len(tracingNotEnabled) == 0 {
+				fmt.Println("No meshed pods found")
+			}
+
+			return nil
+		},
+	}
+	cmd.Flags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "The namespace to list pods in")
+	cmd.Flags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "If present, list pods across all namespaces")
+
+	return cmd
+}
diff --git a/jaeger/cmd/root.go b/jaeger/cmd/root.go
index 183bdd018..8849c07cf 100644
--- a/jaeger/cmd/root.go
+++ b/jaeger/cmd/root.go
@@ -63,10 +63,11 @@ func NewCmdJaeger() *cobra.Command {
 	jaegerCmd.PersistentFlags().StringArrayVar(&impersonateGroup, "as-group", []string{}, "Group to impersonate for Kubernetes operations")
 	jaegerCmd.PersistentFlags().StringVar(&apiAddr, "api-addr", "", "Override kubeconfig and communicate directly with the control plane at host:port (mostly for testing)")
 	jaegerCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Turn on debug logging")
-	jaegerCmd.AddCommand(newCmdInstall())
 	jaegerCmd.AddCommand(NewCmdCheck())
-	jaegerCmd.AddCommand(newCmdUninstall())
 	jaegerCmd.AddCommand(newCmdDashboard())
+	jaegerCmd.AddCommand(newCmdInstall())
+	jaegerCmd.AddCommand(newCmdList())
+	jaegerCmd.AddCommand(newCmdUninstall())
 
 	return jaegerCmd
 }
diff --git a/multicluster/cmd/install.go b/multicluster/cmd/install.go
index 43cf26a28..6b4af5668 100644
--- a/multicluster/cmd/install.go
+++ b/multicluster/cmd/install.go
@@ -73,7 +73,7 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 				ImpersonateGroup:      impersonateGroup,
 				APIAddr:               apiAddr,
 				RetryDeadline:         time.Now().Add(wait),
-			}, true)
+			})
 
 			values, err := buildMulticlusterInstallValues(cmd.Context(), options)
 
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index a5bd66c91..9b9c4da12 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -760,29 +760,6 @@ func (hc *HealthChecker) allCategories() []Category {
 						return checkContainerRunning(hc.controlPlanePods, "controller")
 					},
 				},
-				{
-					description: "can initialize the client",
-					hintAnchor:  "l5d-existence-client",
-					fatal:       true,
-					check: func(ctx context.Context) (err error) {
-						if hc.APIAddr != "" {
-							hc.apiClient, err = public.NewInternalClient(hc.ControlPlaneNamespace, hc.APIAddr)
-						} else {
-							hc.apiClient, err = public.NewExternalClient(ctx, hc.ControlPlaneNamespace, hc.kubeAPI)
-						}
-						return
-					},
-				},
-				{
-					description:   "can query the control plane API",
-					hintAnchor:    "l5d-existence-api",
-					retryDeadline: hc.RetryDeadline,
-					fatal:         true,
-					check: func(ctx context.Context) (err error) {
-						hc.serverVersion, err = GetServerVersion(ctx, hc.apiClient)
-						return
-					},
-				},
 			},
 		},
 		{
@@ -1214,6 +1191,29 @@ func (hc *HealthChecker) allCategories() []Category {
 						return validateControlPlanePods(hc.controlPlanePods)
 					},
 				},
+				{
+					description: "can initialize the client",
+					hintAnchor:  "l5d-api-control-client",
+					fatal:       true,
+					check: func(ctx context.Context) (err error) {
+						if hc.APIAddr != "" {
+							hc.apiClient, err = public.NewInternalClient(hc.ControlPlaneNamespace, hc.APIAddr)
+						} else {
+							hc.apiClient, err = public.NewExternalClient(ctx, hc.ControlPlaneNamespace, hc.kubeAPI)
+						}
+						return
+					},
+				},
+				{
+					description:   "can query the control plane API",
+					hintAnchor:    "l5d-api-control-api",
+					retryDeadline: hc.RetryDeadline,
+					fatal:         true,
+					check: func(ctx context.Context) (err error) {
+						hc.serverVersion, err = GetServerVersion(ctx, hc.apiClient)
+						return
+					},
+				},
 			},
 		},
 		{
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index 7eeb68e87..471d8ab6c 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -916,7 +916,7 @@ func (conf *ResourceConfig) IsNamespace() bool {
 	return strings.ToLower(conf.workload.metaType.Kind) == k8s.Namespace
 }
 
-//IsService checks if a given config is a workload of Kind service
+// IsService checks if a given config is a workload of Kind service
 func (conf *ResourceConfig) IsService() bool {
 	return strings.ToLower(conf.workload.metaType.Kind) == k8s.Service
 }
@@ -942,6 +942,29 @@ func (conf *ResourceConfig) InjectNamespace(annotations map[string]string) ([]by
 	return yaml.JSONToYAML(j)
 }
 
+// AnnotateService returns a Service with the appropriate annotations
+// Currently, a `Service` may only need the `config.linkerd.io/opaque-ports` annotation via `inject`
+// See - https://github.com/linkerd/linkerd2/pull/5721
+func (conf *ResourceConfig) AnnotateService(annotations map[string]string) ([]byte, error) {
+	ns, ok := conf.workload.obj.(*corev1.Service)
+	if !ok {
+		return nil, errors.New("can't inject service. Type assertion failed")
+	}
+
+	//For overriding annotations
+	if len(annotations) > 0 {
+		for annotation, value := range annotations {
+			ns.Annotations[annotation] = value
+		}
+	}
+
+	j, err := getFilteredJSON(ns)
+	if err != nil {
+		return nil, err
+	}
+	return yaml.JSONToYAML(j)
+}
+
 //getFilteredJSON method performs JSON marshaling such that zero values of
 //empty structs are respected by `omitempty` tags. We make use of a drop-in
 //replacement of the standard json/encoding library, without which empty struct values
diff --git a/pkg/inject/uninject.go b/pkg/inject/uninject.go
index d2525efe0..afe7c431f 100644
--- a/pkg/inject/uninject.go
+++ b/pkg/inject/uninject.go
@@ -11,7 +11,7 @@ import (
 // Uninject removes from the workload in conf the init and proxy containers,
 // the TLS volumes and the extra annotations/labels that were added
 func (conf *ResourceConfig) Uninject(report *Report) ([]byte, error) {
-	if conf.IsNamespace() {
+	if conf.IsNamespace() || conf.IsService() {
 		uninjectObjectMeta(conf.workload.Meta, report)
 		return conf.YamlMarshalObj()
 	}
diff --git a/pkg/opaqueports/defaults.go b/pkg/opaqueports/defaults.go
deleted file mode 100644
index 11b142553..000000000
--- a/pkg/opaqueports/defaults.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package opaqueports
-
-// DefaultOpaquePorts is the default list of opaque ports that the destination
-// server will use to determine whether a destination is an opaque protocol.
-// When a pod or service already has its own annotation, that value will have
-// priority of this.
-//
-// Note: Keep in sync with proxy.opaquePorts in values.yaml
-var DefaultOpaquePorts = map[uint32]struct{}{
-	25:    {},
-	443:   {},
-	587:   {},
-	3306:  {},
-	5432:  {},
-	11211: {},
-}
diff --git a/pkg/public/api.go b/pkg/public/api.go
index d88f64bf4..f45197c38 100644
--- a/pkg/public/api.go
+++ b/pkg/public/api.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"time"
 
 	"github.com/linkerd/linkerd2/controller/api/public"
 	pb "github.com/linkerd/linkerd2/controller/gen/public"
@@ -21,26 +20,15 @@ func RawPublicAPIClient(ctx context.Context, kubeAPI *k8s.KubernetesAPI, control
 	return public.NewExternalClient(ctx, controlPlaneNamespace, kubeAPI)
 }
 
-// CheckPublicAPIClientOrExit builds a new Public API client and executes default status
-// checks to determine if the client can successfully perform cli commands. If the
-// checks fail, then CLI will print an error and exit.
-func CheckPublicAPIClientOrExit(hcOptions healthcheck.Options) public.Client {
-	hcOptions.RetryDeadline = time.Time{}
-	return CheckPublicAPIClientOrRetryOrExit(hcOptions, false)
-}
-
 // CheckPublicAPIClientOrRetryOrExit builds a new Public API client and executes status
 // checks to determine if the client can successfully connect to the API. If the
 // checks fail, then CLI will print an error and exit. If the hcOptions.retryDeadline
 // param is specified, then the CLI will print a message to stderr and retry.
-func CheckPublicAPIClientOrRetryOrExit(hcOptions healthcheck.Options, apiChecks bool) public.Client {
+func CheckPublicAPIClientOrRetryOrExit(hcOptions healthcheck.Options) public.Client {
 	checks := []healthcheck.CategoryID{
 		healthcheck.KubernetesAPIChecks,
 		healthcheck.LinkerdControlPlaneExistenceChecks,
-	}
-
-	if apiChecks {
-		checks = append(checks, healthcheck.LinkerdAPIChecks)
+		healthcheck.LinkerdAPIChecks,
 	}
 
 	hc := healthcheck.NewHealthChecker(checks, &hcOptions)
diff --git a/pkg/util/parsing.go b/pkg/util/parsing.go
index 7f1fba0a0..4ee6de48c 100644
--- a/pkg/util/parsing.go
+++ b/pkg/util/parsing.go
@@ -10,6 +10,28 @@ import (
 	corev1 "k8s.io/api/core/v1"
 )
 
+// ParsePorts parses the given ports string into a map of ports;
+// this includes converting port ranges into separate ports
+func ParsePorts(portsString string) (map[uint32]struct{}, error) {
+	opaquePorts := make(map[uint32]struct{})
+	if portsString != "" {
+		portRanges := GetPortRanges(portsString)
+		for _, portRange := range portRanges {
+			pr := portRange.GetPortRange()
+			portsRange, err := ports.ParsePortRange(pr)
+			if err != nil {
+				log.Warnf("Invalid port range [%v]: %s", pr, err)
+				continue
+			}
+			for i := portsRange.LowerBound; i <= portsRange.UpperBound; i++ {
+				opaquePorts[uint32(i)] = struct{}{}
+			}
+
+		}
+	}
+	return opaquePorts, nil
+}
+
 // ParseContainerOpaquePorts parses the opaque ports annotation into a list of ports;
 // this includes converting port ranges into separate ports and named ports
 // into their port number equivalents.
diff --git a/viz/charts/linkerd-viz/templates/tap-injector-rbac.yaml b/viz/charts/linkerd-viz/templates/tap-injector-rbac.yaml
index 28438ae56..75d58d031 100644
--- a/viz/charts/linkerd-viz/templates/tap-injector-rbac.yaml
+++ b/viz/charts/linkerd-viz/templates/tap-injector-rbac.yaml
@@ -45,6 +45,8 @@ metadata:
   namespace: {{ .Values.namespace }}
   annotations:
     {{ include "partials.annotations.created-by" . }}
+  labels:
+    linkerd.io/extension: viz
 type: kubernetes.io/tls
 data:
   tls.crt: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.tapInjector.crtPEM)) (empty .Values.tapInjector.crtPEM) }}
diff --git a/viz/charts/linkerd-viz/templates/tap-injector.yaml b/viz/charts/linkerd-viz/templates/tap-injector.yaml
index 3dc314f11..e31623345 100644
--- a/viz/charts/linkerd-viz/templates/tap-injector.yaml
+++ b/viz/charts/linkerd-viz/templates/tap-injector.yaml
@@ -15,6 +15,7 @@ metadata:
 spec:
   type: ClusterIP
   selector:
+    linkerd.io/extension: viz
     component: tap-injector
   ports:
   - name: tap-injector
diff --git a/viz/cmd/install.go b/viz/cmd/install.go
index f956ea286..a5ed3cb57 100644
--- a/viz/cmd/install.go
+++ b/viz/cmd/install.go
@@ -67,7 +67,7 @@ A full list of configurable values can be found at https://www.github.com/linker
 					ImpersonateGroup:      impersonateGroup,
 					APIAddr:               apiAddr,
 					RetryDeadline:         time.Now().Add(wait),
-				}, true)
+				})
 
 			}
 			return install(os.Stdout, options, ha)
diff --git a/viz/cmd/list.go b/viz/cmd/list.go
new file mode 100644
index 000000000..2abe868a4
--- /dev/null
+++ b/viz/cmd/list.go
@@ -0,0 +1,94 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
+	vizLabels "github.com/linkerd/linkerd2/viz/pkg/labels"
+	"github.com/spf13/cobra"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type listOptions struct {
+	namespace     string
+	allNamespaces bool
+}
+
+func newCmdList() *cobra.Command {
+	var options listOptions
+
+	cmd := &cobra.Command{
+		Use:   "list [flags]",
+		Short: "Lists which pods can be tapped",
+		Args:  cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return err
+			}
+
+			if options.namespace == "" {
+				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
+			}
+			if options.allNamespaces {
+				options.namespace = v1.NamespaceAll
+			}
+
+			pods, err := k8sAPI.CoreV1().Pods(options.namespace).List(cmd.Context(), metav1.ListOptions{})
+			if err != nil {
+				fmt.Fprintln(os.Stderr, err)
+				os.Exit(1)
+			}
+
+			var tapEnabled, tapDisabled, tapNotEnabled []v1.Pod
+
+			for _, pod := range pods.Items {
+				pod := pod
+				if pkgK8s.IsMeshed(&pod, controlPlaneNamespace) {
+					if vizLabels.IsTapDisabled(pod) {
+						tapDisabled = append(tapDisabled, pod)
+					} else if !vizLabels.IsTapEnabled(&pod) {
+						tapNotEnabled = append(tapNotEnabled, pod)
+					} else {
+						tapEnabled = append(tapEnabled, pod)
+					}
+				}
+			}
+
+			if len(tapEnabled) > 0 {
+				fmt.Println("Pods with tap enabled:")
+				for _, pod := range tapEnabled {
+					fmt.Printf("\t* %s/%s\n", pod.Namespace, pod.Name)
+				}
+			}
+
+			if len(tapDisabled) > 0 {
+				fmt.Println("Pods with tap disabled:")
+				for _, pod := range tapDisabled {
+					fmt.Printf("\t* %s/%s\n", pod.Namespace, pod.Name)
+				}
+			}
+
+			if len(tapNotEnabled) > 0 {
+				fmt.Println("Pods missing tap configuration (restart these pods to enable tap):")
+				for _, pod := range tapNotEnabled {
+					fmt.Printf("\t* %s/%s\n", pod.Namespace, pod.Name)
+				}
+			}
+
+			if len(tapEnabled)+len(tapDisabled)+len(tapNotEnabled) == 0 {
+				fmt.Println("No meshed pods found")
+			}
+
+			return nil
+		},
+	}
+	cmd.Flags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "The namespace to list pods in")
+	cmd.Flags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "If present, list pods across all namespaces")
+
+	return cmd
+}
diff --git a/viz/cmd/root.go b/viz/cmd/root.go
index 75134d26e..f5ecb19d4 100644
--- a/viz/cmd/root.go
+++ b/viz/cmd/root.go
@@ -70,16 +70,17 @@ func NewCmdViz() *cobra.Command {
 	vizCmd.PersistentFlags().StringArrayVar(&impersonateGroup, "as-group", []string{}, "Group to impersonate for Kubernetes operations")
 	vizCmd.PersistentFlags().StringVar(&apiAddr, "api-addr", "", "Override kubeconfig and communicate directly with the control plane at host:port (mostly for testing)")
 	vizCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Turn on debug logging")
+	vizCmd.AddCommand(NewCmdCheck())
+	vizCmd.AddCommand(NewCmdDashboard())
+	vizCmd.AddCommand(NewCmdEdges())
 	vizCmd.AddCommand(newCmdInstall())
+	vizCmd.AddCommand(newCmdList())
+	vizCmd.AddCommand(newCmdProfile())
 	vizCmd.AddCommand(NewCmdRoutes())
 	vizCmd.AddCommand(NewCmdStat())
 	vizCmd.AddCommand(NewCmdTap())
 	vizCmd.AddCommand(NewCmdTop())
-	vizCmd.AddCommand(NewCmdEdges())
-	vizCmd.AddCommand(NewCmdDashboard())
 	vizCmd.AddCommand(newCmdUninstall())
-	vizCmd.AddCommand(newCmdProfile())
-	vizCmd.AddCommand(NewCmdCheck())
 
 	return vizCmd
 }
diff --git a/viz/pkg/healthcheck/healthcheck.go b/viz/pkg/healthcheck/healthcheck.go
index 2d94000c0..3417d6d69 100644
--- a/viz/pkg/healthcheck/healthcheck.go
+++ b/viz/pkg/healthcheck/healthcheck.go
@@ -212,7 +212,7 @@ func (hc *HealthChecker) VizCategory() healthcheck.Category {
 				return
 			}),
 		*healthcheck.NewChecker("viz extension self-check").
-			WithHintAnchor("l5d-api-control-api").
+			WithHintAnchor("l5d-viz-metrics-api").
 			Fatal().
 			// to avoid confusing users with a prometheus readiness error, we only show
 			// "waiting for check to complete" while things converge. If after the timeout
diff --git a/web/app/js/components/Tap.jsx b/web/app/js/components/Tap.jsx
index b9cbbc30b..47238b955 100644
--- a/web/app/js/components/Tap.jsx
+++ b/web/app/js/components/Tap.jsx
@@ -5,6 +5,7 @@ import { handlePageVisibility, withPageVisibility } from './util/PageVisibility.
 import ErrorBanner from './ErrorBanner.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
+import TapEnabledWarning from './TapEnabledWarning.jsx';
 import TapEventTable from './TapEventTable.jsx';
 import TapQueryForm from './TapQueryForm.jsx';
 import _cloneDeep from 'lodash/cloneDeep';
@@ -50,6 +51,7 @@ class Tap extends React.Component {
       tapIsClosing: false,
       pollingInterval: 10000,
       pendingRequests: false,
+      showTapEnabledWarning: false,
     };
   }
 
@@ -126,6 +128,10 @@ class Tap extends React.Component {
             error: e.reason,
           },
         });
+      } else if (e.reason.includes('no pods to tap')) {
+        this.setState({
+          showTapEnabledWarning: true,
+        });
       } else {
         this.setState({
           error: {
@@ -248,6 +254,7 @@ class Tap extends React.Component {
     this.setState({
       tapResultsById: {},
       query: emptyTapQuery(),
+      showTapEnabledWarning: false,
     });
   }
 
@@ -291,11 +298,12 @@ class Tap extends React.Component {
   updateQuery = query => {
     this.setState({
       query,
+      showTapEnabledWarning: false,
     });
   }
 
   render() {
-    const { tapResultsById, tapRequestInProgress, tapIsClosing, resourcesByNs, authoritiesByNs, query, error } = this.state;
+    const { tapResultsById, tapRequestInProgress, tapIsClosing, resourcesByNs, authoritiesByNs, query, showTapEnabledWarning, error } = this.state;
     const tableRows = _orderBy(_values(tapResultsById), r => r.lastUpdated, 'desc');
 
     return (
@@ -314,10 +322,17 @@ class Tap extends React.Component {
           authoritiesByNs={authoritiesByNs}
           updateQuery={this.updateQuery}
           currentQuery={query} />
-
-        <TapEventTable
-          resource={query.resource}
-          tableRows={tableRows} />
+        {showTapEnabledWarning &&
+          <TapEnabledWarning
+            resource={query.resource}
+            namespace={query.namespace}
+            cardComponent />
+        }
+        {!showTapEnabledWarning &&
+          <TapEventTable
+            resource={query.resource}
+            tableRows={tableRows} />
+        }
       </div>
     );
   }
diff --git a/web/app/js/components/TapEnabledWarning.jsx b/web/app/js/components/TapEnabledWarning.jsx
new file mode 100644
index 000000000..aa0e254a6
--- /dev/null
+++ b/web/app/js/components/TapEnabledWarning.jsx
@@ -0,0 +1,53 @@
+import Card from '@material-ui/core/Card';
+import CardContent from '@material-ui/core/CardContent';
+import Grid from '@material-ui/core/Grid';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Trans } from '@lingui/macro';
+import Warning from '@material-ui/icons/Warning';
+import { withStyles } from '@material-ui/core/styles';
+
+const styles = theme => ({
+  cardContainer: {
+    marginTop: theme.spacing(1.5),
+  },
+  container: {
+    margin: theme.spacing(1),
+  },
+  iconWarning: {
+    color: theme.status.dark.warning,
+  },
+});
+
+const TapEnabledWarning = ({ resource, cardComponent, namespace, classes }) => {
+  const component = (
+    <Grid className={classes.container} container spacing={1} alignItems="center">
+      <Grid item><Warning className={classes.iconWarning} /></Grid>
+      <Grid item>
+        <Trans>Pods under the resource {resource} in the {namespace} namespace are missing tap configurations (restart these pods to enable tap)</Trans>
+      </Grid>
+    </Grid>
+  );
+
+  if (cardComponent) {
+    return (
+      <Card className={classes.cardContainer}>
+        <CardContent>{component}</CardContent>
+      </Card>
+    );
+  } else {
+    return component;
+  }
+};
+
+TapEnabledWarning.propTypes = {
+  resource: PropTypes.string.isRequired,
+  namespace: PropTypes.string.isRequired,
+  cardComponent: PropTypes.bool,
+};
+
+TapEnabledWarning.defaultProps = {
+  cardComponent: false,
+};
+
+export default withStyles(styles)(TapEnabledWarning);
diff --git a/web/app/js/components/TapQueryForm.jsx b/web/app/js/components/TapQueryForm.jsx
index 2ef4f90d7..b525ec9bf 100644
--- a/web/app/js/components/TapQueryForm.jsx
+++ b/web/app/js/components/TapQueryForm.jsx
@@ -7,13 +7,13 @@ import {
   tapQueryProps,
   tapResourceTypes,
 } from './util/TapUtils.jsx';
+import Accordion from '@material-ui/core/Accordion';
+import AccordionDetails from '@material-ui/core/AccordionDetails';
+import AccordionSummary from '@material-ui/core/AccordionSummary';
 import Button from '@material-ui/core/Button';
 import Card from '@material-ui/core/Card';
 import CardContent from '@material-ui/core/CardContent';
 import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
-import ExpansionPanel from '@material-ui/core/ExpansionPanel';
-import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
-import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
 import FormControl from '@material-ui/core/FormControl';
 import FormHelperText from '@material-ui/core/FormHelperText';
 import Grid from '@material-ui/core/Grid';
@@ -356,25 +356,25 @@ class TapQueryForm extends React.Component {
                 inputProps={{ name: 'authority', id: 'authority' }}
                 className={classes.selectEmpty}>
                 {
-                _map(autocomplete.authority, (d, i) => (
-                  <MenuItem key={`authority-${i}`} value={d}>{d}</MenuItem>
-                ))
-              }
+                  _map(autocomplete.authority, (d, i) => (
+                    <MenuItem key={`authority-${i}`} value={d}>{d}</MenuItem>
+                  ))
+                }
               </Select>
               <FormHelperText><Trans>formAuthorityHelpText</Trans></FormHelperText>
             </FormControl>
           </Grid>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
-            { this.renderTextInput(<Trans>formPath</Trans>, 'path', <Trans>formPathHelpText</Trans>) }
+            {this.renderTextInput(<Trans>formPath</Trans>, 'path', <Trans>formPathHelpText</Trans>)}
           </Grid>
         </Grid>
 
         <Grid container spacing={3}>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
-            { this.renderTextInput(<Trans>formScheme</Trans>, 'scheme', <Trans>formSchemeHelpText</Trans>) }
+            {this.renderTextInput(<Trans>formScheme</Trans>, 'scheme', <Trans>formSchemeHelpText</Trans>)}
           </Grid>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
-            { this.renderTextInput(<Trans>formMaxRPS</Trans>, 'maxRps', <Trans>formMaxRPSHelpText {defaultMaxRps}</Trans>) }
+            {this.renderTextInput(<Trans>formMaxRPS</Trans>, 'maxRps', <Trans>formMaxRPSHelpText {defaultMaxRps}</Trans>)}
           </Grid>
           <Grid item xs={6} md={3} className={classes.formControlWrapper}>
             <FormControl className={classes.formControl}>
@@ -385,10 +385,10 @@ class TapQueryForm extends React.Component {
                 inputProps={{ name: 'method', id: 'method' }}
                 className={classes.selectEmpty}>
                 {
-                _map(httpMethods, d => (
-                  <MenuItem key={`method-${d}`} value={d}>{d}</MenuItem>
-                ))
-              }
+                  _map(httpMethods, d => (
+                    <MenuItem key={`method-${d}`} value={d}>{d}</MenuItem>
+                  ))
+                }
               </Select>
               <FormHelperText><Trans>formHTTPMethodHelpText</Trans></FormHelperText>
             </FormControl>
@@ -403,17 +403,17 @@ class TapQueryForm extends React.Component {
     const { advancedFormExpanded } = this.state;
 
     return (
-      <ExpansionPanel expanded={advancedFormExpanded} onChange={this.handleAdvancedFormExpandClick} elevation={3}>
-        <ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
+      <Accordion expanded={advancedFormExpanded} onChange={this.handleAdvancedFormExpandClick} elevation={3}>
+        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
           <Typography variant="caption" gutterBottom>
             {advancedFormExpanded ? <Trans>formHideFilters</Trans> : <Trans>formShowFilters</Trans>}
           </Typography>
-        </ExpansionPanelSummary>
+        </AccordionSummary>
 
-        <ExpansionPanelDetails>
+        <AccordionDetails>
           {this.renderAdvancedTapFormContent()}
-        </ExpansionPanelDetails>
-      </ExpansionPanel>
+        </AccordionDetails>
+      </Accordion>
     );
   }
 
@@ -438,7 +438,7 @@ class TapQueryForm extends React.Component {
             </Grid>
 
             <Grid item>
-              { this.renderTapButton(tapRequestInProgress, tapIsClosing) }
+              {this.renderTapButton(tapRequestInProgress, tapIsClosing)}
               <Button onClick={this.resetTapForm} disabled={tapRequestInProgress} className={classes.resetButton}>
                 <Trans>buttonReset</Trans>
               </Button>
@@ -448,7 +448,7 @@ class TapQueryForm extends React.Component {
 
         <QueryToCliCmd cmdName={cmdName} query={query} resource={query.resource} />
 
-        { !enableAdvancedForm ? null : this.renderAdvancedTapForm() }
+        {!enableAdvancedForm ? null : this.renderAdvancedTapForm()}
 
       </Card>
     );
diff --git a/web/app/js/components/Top.jsx b/web/app/js/components/Top.jsx
index f95bc942e..35765181e 100644
--- a/web/app/js/components/Top.jsx
+++ b/web/app/js/components/Top.jsx
@@ -2,6 +2,7 @@ import { handlePageVisibility, withPageVisibility } from './util/PageVisibility.
 import ErrorBanner from './ErrorBanner.jsx';
 import PropTypes from 'prop-types';
 import React from 'react';
+import TapEnabledWarning from './TapEnabledWarning.jsx';
 import TapQueryForm from './TapQueryForm.jsx';
 import TopModule from './TopModule.jsx';
 import _each from 'lodash/each';
@@ -179,11 +180,14 @@ class Top extends React.Component {
           tapIsClosing={tapIsClosing}
           updateQuery={this.updateQuery}
           currentQuery={query} />
-
         <TopModule
           pathPrefix={pathPrefix}
           query={query}
           startTap={tapRequestInProgress}
+          tapEnabledWarningComponent={<TapEnabledWarning
+            resource={query.resource}
+            namespace={query.namespace}
+            cardComponent />}
           updateTapClosingState={this.updateTapClosingState} />
       </div>
     );
diff --git a/web/app/js/components/TopModule.jsx b/web/app/js/components/TopModule.jsx
index d5429bc92..56bf74a86 100644
--- a/web/app/js/components/TopModule.jsx
+++ b/web/app/js/components/TopModule.jsx
@@ -35,6 +35,7 @@ class TopModule extends React.Component {
     this.state = {
       error: null,
       topEventIndex: {},
+      showTapEnabledWarning: false,
     };
   }
 
@@ -91,11 +92,17 @@ class TopModule extends React.Component {
     https://github.com/linkerd/linkerd2/issues/1630
     */
     if (e.code !== WS_NORMAL_CLOSURE && e.code !== WS_ABNORMAL_CLOSURE) {
-      this.setState({
-        error: {
-          error: `Websocket close error [${e.code}: ${wsCloseCodes[e.code]}] ${e.reason ? ':' : ''} ${e.reason}`,
-        },
-      });
+      if (e.reason.includes('no pods to tap')) {
+        this.setState({
+          showTapEnabledWarning: true,
+        });
+      } else {
+        this.setState({
+          error: {
+            error: `Websocket close error [${e.code}: ${wsCloseCodes[e.code]}] ${e.reason ? ':' : ''} ${e.reason}`,
+          },
+        });
+      }
     }
   }
 
@@ -321,6 +328,7 @@ class TopModule extends React.Component {
     this.topEventIndex = {};
     this.setState({
       topEventIndex: {},
+      showTapEnabledWarning: false,
     });
   }
 
@@ -349,8 +357,8 @@ class TopModule extends React.Component {
   }
 
   render() {
-    const { topEventIndex } = this.state;
-    const { query, maxRowsToDisplay } = this.props;
+    const { topEventIndex, showTapEnabledWarning } = this.state;
+    const { query, maxRowsToDisplay, tapEnabledWarningComponent } = this.props;
 
     const tableRows = _take(_values(topEventIndex), maxRowsToDisplay);
     const resourceType = _isNil(query.resource) ? '' : query.resource.split('/')[0];
@@ -358,7 +366,8 @@ class TopModule extends React.Component {
     return (
       <React.Fragment>
         {this.banner()}
-        <TopEventTable resourceType={resourceType} tableRows={tableRows} />
+        {showTapEnabledWarning && tapEnabledWarningComponent}
+        {!showTapEnabledWarning && <TopEventTable resourceType={resourceType} tableRows={tableRows} />}
       </React.Fragment>
     );
   }
@@ -370,8 +379,10 @@ TopModule.propTypes = {
   pathPrefix: PropTypes.string.isRequired,
   query: PropTypes.shape({
     resource: PropTypes.string,
+    namespace: PropTypes.string,
   }),
   startTap: PropTypes.bool.isRequired,
+  tapEnabledWarningComponent: PropTypes.node,
   updateTapClosingState: PropTypes.func,
   updateUnmeshedSources: PropTypes.func,
 };
@@ -387,7 +398,9 @@ TopModule.defaultProps = {
   updateUnmeshedSources: _noop,
   query: {
     resource: '',
+    namespace: '',
   },
+  tapEnabledWarningComponent: null,
 };
 
 export default withContext(TopModule);
diff --git a/web/app/js/locales/en/messages.json b/web/app/js/locales/en/messages.json
index 7d572ce39..42797ab0d 100644
--- a/web/app/js/locales/en/messages.json
+++ b/web/app/js/locales/en/messages.json
@@ -155,6 +155,7 @@
   "tableTitleResponseInit": "Response Init",
   "tableTitleTCP": "TCP",
   "tableTitleTCPMetrics": "TCP Metrics",
+  "Pods under the resource {resource} in the {namespace} namespace are missing tap configurations (restart these pods to enable tap)": "Pods under the resource {resource} in the {namespace} namespace are missing tap configurations (restart these pods to enable tap)",
   "tooltipInbound": "INBOUND",
   "tooltipOutbound": "OUTBOUND",
   "unspecifiedResourcesMsg": "one or more resources",
diff --git a/web/app/js/locales/es/messages.json b/web/app/js/locales/es/messages.json
index e6aecac56..cbd6d03d7 100644
--- a/web/app/js/locales/es/messages.json
+++ b/web/app/js/locales/es/messages.json
@@ -155,6 +155,7 @@
   "tableTitleResponseInit": "Respuesta Inicial",
   "tableTitleTCP": "TCP",
   "tableTitleTCPMetrics": "Métricas TCP",
+  "Pods under the resource {resource} in the {namespace} namespace are missing tap configurations (restart these pods to enable tap)":"A los pods debajo del recurso {resource} en el namespace {namespace} les hace falta la configuración de tap (reinicie estos pods para habilitar tap)",
   "tooltipInbound": "ENTRADA",
   "tooltipOutbound": "SALIDA",
   "unspecifiedResourcesMsg": "uno o más recursos",


Test patch:
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index a917fd4f1..f72c24e49 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -106,7 +106,7 @@ jobs:
         - helm-upgrade
         - multicluster
         - uninstall
-        - upgrade-edge
+        #- upgrade-edge
         - upgrade-stable
         - cni-calico-deep
     needs: [docker_build]
diff --git a/cli/cmd/inject_test.go b/cli/cmd/inject_test.go
index a3609359c..dc77c19c3 100644
--- a/cli/cmd/inject_test.go
+++ b/cli/cmd/inject_test.go
@@ -92,6 +92,9 @@ func TestUninjectAndInject(t *testing.T) {
 	cniEnabledConfig := defaultConfig()
 	cniEnabledConfig.CNIEnabled = true
 
+	opaquePortsConfig := defaultConfig()
+	opaquePortsConfig.Proxy.OpaquePorts = "3000,5000-6000,mysql"
+
 	proxyIgnorePortsConfig := defaultConfig()
 	proxyIgnorePortsConfig.ProxyInit.IgnoreInboundPorts = "22,8100-8102"
 	proxyIgnorePortsConfig.ProxyInit.IgnoreOutboundPorts = "5432"
@@ -289,6 +292,13 @@ func TestUninjectAndInject(t *testing.T) {
 			injectProxy:      true,
 			testInjectConfig: proxyIgnorePortsConfig,
 		},
+		{
+			inputFileName:    "inject_emojivoto_deployment.input.yml",
+			goldenFileName:   "inject_emojivoto_deployment_opaque_ports.golden.yml",
+			reportFileName:   "inject_emojivoto_deployment_opaque_ports.report",
+			injectProxy:      true,
+			testInjectConfig: opaquePortsConfig,
+		},
 	}
 
 	for i, tc := range testCases {
diff --git a/cli/cmd/install_test.go b/cli/cmd/install_test.go
index f4c790c3a..5ee4f0978 100644
--- a/cli/cmd/install_test.go
+++ b/cli/cmd/install_test.go
@@ -78,7 +78,8 @@ func TestRender(t *testing.T) {
 				Inbound:  4143,
 				Outbound: 4140,
 			},
-			UID: 2102,
+			UID:         2102,
+			OpaquePorts: "25,443,587,3306,5432,11211",
 		},
 		ProxyInit: &charts.ProxyInit{
 			Image: &charts.Image{
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.golden.yml b/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.golden.yml
new file mode 100644
index 000000000..affacff64
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.golden.yml
@@ -0,0 +1,179 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: web
+  namespace: emojivoto
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: web-svc
+  template:
+    metadata:
+      annotations:
+        config.linkerd.io/opaque-ports: 3000,5000-6000,mysql
+        linkerd.io/created-by: linkerd/cli dev-undefined
+        linkerd.io/identity-mode: default
+        linkerd.io/proxy-version: test-inject-proxy-version
+      labels:
+        app: web-svc
+        linkerd.io/control-plane-ns: linkerd
+        linkerd.io/proxy-deployment: web
+        linkerd.io/workload-ns: emojivoto
+    spec:
+      containers:
+      - env:
+        - name: WEB_PORT
+          value: "80"
+        - name: EMOJISVC_HOST
+          value: emoji-svc.emojivoto:8080
+        - name: VOTINGSVC_HOST
+          value: voting-svc.emojivoto:8080
+        - name: INDEX_BUNDLE
+          value: dist/index_bundle.js
+        image: buoyantio/emojivoto-web:v10
+        name: web-svc
+        ports:
+        - containerPort: 80
+          name: http
+      - env:
+        - name: LINKERD2_PROXY_LOG
+          value: warn,linkerd=info
+        - name: LINKERD2_PROXY_LOG_FORMAT
+          value: plain
+        - name: LINKERD2_PROXY_DESTINATION_SVC_ADDR
+          value: linkerd-dst-headless.linkerd.svc.cluster.local.:8086
+        - name: LINKERD2_PROXY_DESTINATION_PROFILE_NETWORKS
+          value: 10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16
+        - name: LINKERD2_PROXY_INBOUND_CONNECT_TIMEOUT
+          value: 100ms
+        - name: LINKERD2_PROXY_OUTBOUND_CONNECT_TIMEOUT
+          value: 1000ms
+        - name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR
+          value: 0.0.0.0:4190
+        - name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR
+          value: 0.0.0.0:4191
+        - name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR
+          value: 127.0.0.1:4140
+        - name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR
+          value: 0.0.0.0:4143
+        - name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES
+          value: svc.cluster.local.
+        - name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE
+          value: 10000ms
+        - name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE
+          value: 10000ms
+        - name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION
+          value: 3000,5000-6000,mysql
+        - name: _pod_ns
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: _pod_nodeName
+          valueFrom:
+            fieldRef:
+              fieldPath: spec.nodeName
+        - name: LINKERD2_PROXY_DESTINATION_CONTEXT
+          value: |
+            {"ns":"$(_pod_ns)", "nodeName":"$(_pod_nodeName)"}
+        - name: LINKERD2_PROXY_IDENTITY_DIR
+          value: /var/run/linkerd/identity/end-entity
+        - name: LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS
+          value: |
+            -----BEGIN CERTIFICATE-----
+            MIIBwTCCAWagAwIBAgIQeDZp5lDaIygQ5UfMKZrFATAKBggqhkjOPQQDAjApMScw
+            JQYDVQQDEx5pZGVudGl0eS5saW5rZXJkLmNsdXN0ZXIubG9jYWwwHhcNMjAwODI4
+            MDcxMjQ3WhcNMzAwODI2MDcxMjQ3WjApMScwJQYDVQQDEx5pZGVudGl0eS5saW5r
+            ZXJkLmNsdXN0ZXIubG9jYWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARqc70Z
+            l1vgw79rjB5uSITICUA6GyfvSFfcuIis7B/XFSkkwAHU5S/s1AAP+R0TX7HBWUC4
+            uaG4WWsiwJKNn7mgo3AwbjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB
+            /wIBATAdBgNVHQ4EFgQU5YtjVVPfd7I7NLHsn2C26EByGV0wKQYDVR0RBCIwIIIe
+            aWRlbnRpdHkubGlua2VyZC5jbHVzdGVyLmxvY2FsMAoGCCqGSM49BAMCA0kAMEYC
+            IQCN7lBFLDDvjx6V0+XkjpKERRsJYf5adMvnloFl48ilJgIhANtxhndcr+QJPuC8
+            vgUC0d2/9FMueIVMb+46WTCOjsqr
+            -----END CERTIFICATE-----
+        - name: LINKERD2_PROXY_IDENTITY_TOKEN_FILE
+          value: /var/run/secrets/kubernetes.io/serviceaccount/token
+        - name: LINKERD2_PROXY_IDENTITY_SVC_ADDR
+          value: linkerd-identity-headless.linkerd.svc.cluster.local.:8080
+        - name: _pod_sa
+          valueFrom:
+            fieldRef:
+              fieldPath: spec.serviceAccountName
+        - name: _l5d_ns
+          value: linkerd
+        - name: _l5d_trustdomain
+          value: cluster.local
+        - name: LINKERD2_PROXY_IDENTITY_LOCAL_NAME
+          value: $(_pod_sa).$(_pod_ns).serviceaccount.identity.$(_l5d_ns).$(_l5d_trustdomain)
+        - name: LINKERD2_PROXY_IDENTITY_SVC_NAME
+          value: linkerd-identity.$(_l5d_ns).serviceaccount.identity.$(_l5d_ns).$(_l5d_trustdomain)
+        - name: LINKERD2_PROXY_DESTINATION_SVC_NAME
+          value: linkerd-destination.$(_l5d_ns).serviceaccount.identity.$(_l5d_ns).$(_l5d_trustdomain)
+        image: cr.l5d.io/linkerd/proxy:test-inject-proxy-version
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          httpGet:
+            path: /live
+            port: 4191
+          initialDelaySeconds: 10
+        name: linkerd-proxy
+        ports:
+        - containerPort: 4143
+          name: linkerd-proxy
+        - containerPort: 4191
+          name: linkerd-admin
+        readinessProbe:
+          httpGet:
+            path: /ready
+            port: 4191
+          initialDelaySeconds: 2
+        securityContext:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
+          runAsUser: 2102
+        terminationMessagePolicy: FallbackToLogsOnError
+        volumeMounts:
+        - mountPath: /var/run/linkerd/identity/end-entity
+          name: linkerd-identity-end-entity
+      initContainers:
+      - args:
+        - --incoming-proxy-port
+        - "4143"
+        - --outgoing-proxy-port
+        - "4140"
+        - --proxy-uid
+        - "2102"
+        - --inbound-ports-to-ignore
+        - 4190,4191
+        image: cr.l5d.io/linkerd/proxy-init:v1.3.9
+        imagePullPolicy: IfNotPresent
+        name: linkerd-init
+        resources:
+          limits:
+            cpu: 100m
+            memory: 50Mi
+          requests:
+            cpu: 10m
+            memory: 10Mi
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            add:
+            - NET_ADMIN
+            - NET_RAW
+          privileged: false
+          readOnlyRootFilesystem: true
+          runAsNonRoot: false
+          runAsUser: 0
+        terminationMessagePolicy: FallbackToLogsOnError
+        volumeMounts:
+        - mountPath: /run
+          name: linkerd-proxy-init-xtables-lock
+      volumes:
+      - emptyDir: {}
+        name: linkerd-proxy-init-xtables-lock
+      - emptyDir:
+          medium: Memory
+        name: linkerd-identity-end-entity
+---
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.report b/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.report
new file mode 100644
index 000000000..99851e468
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.report
@@ -0,0 +1,3 @@
+
+deployment "web" injected
+
diff --git a/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.report.verbose b/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.report.verbose
new file mode 100644
index 000000000..59734f772
--- /dev/null
+++ b/cli/cmd/testdata/inject_emojivoto_deployment_opaque_ports.report.verbose
@@ -0,0 +1,10 @@
+
+√ 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
+√ pods do not have automountServiceAccountToken set to "false"
+
+deployment "web" injected
+
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index b67acbe96..deb04f448 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -1619,6 +1619,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         - -trace-collector=collector.linkerd-jaeger.svc.cluster.local:55678
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index 6da98a44a..18524aa90 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -1617,6 +1617,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 71e1e2718..104711aa4 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -1617,6 +1617,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: my.custom.registry/linkerd-io/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 811a82bc9..94649d6d9 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -1617,6 +1617,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_default_override_dst_get_nets.golden b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
index 37c2b3bfa..93d971b1b 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -1617,6 +1617,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 943372ff1..6ea3bd78a 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -1792,6 +1792,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 7f21547f3..e457b3ce5 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -1792,6 +1792,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 991aa9d41..3ac825c96 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -1545,6 +1545,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index 923177b0c..4c3c01cc6 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -1603,6 +1603,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=test.trust.domain
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:linkerd-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index 8135f2b5f..dcd81da22 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -1778,6 +1778,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=test.trust.domain
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:linkerd-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index c053d9d61..75cc3ead9 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -1794,6 +1794,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=test.trust.domain
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:linkerd-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
index 03682e791..47dd4fc3c 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -1778,6 +1778,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=test.trust.domain
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:linkerd-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 7345bccd2..95bb08791 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -1538,6 +1538,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 2a02b20cd..e79feddbb 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -966,7 +966,7 @@ data:
       isIngress: false
       logFormat: plain
       logLevel: warn,linkerd=info
-      opaquePorts: ""
+      opaquePorts: 25,443,587,3306,5432,11211
       outboundConnectTimeout: ""
       ports:
         admin: 4191
@@ -1177,6 +1177,8 @@ spec:
           value: 10000ms
         - name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE
           value: 10000ms
+        - name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION
+          value: "25,443,587,3306,5432,11211"
         - name: _pod_ns
           valueFrom:
             fieldRef:
@@ -1409,6 +1411,8 @@ spec:
           value: 10000ms
         - name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE
           value: 10000ms
+        - name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION
+          value: "25,443,587,3306,5432,11211"
         - name: _pod_ns
           valueFrom:
             fieldRef:
@@ -1617,6 +1621,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: ControllerImage:ControllerImageVersion
         imagePullPolicy: ImagePullPolicy
         livenessProbe:
@@ -1660,6 +1665,8 @@ spec:
           value: 10000ms
         - name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE
           value: 10000ms
+        - name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION
+          value: "25,443,587,3306,5432,11211"
         - name: _pod_ns
           valueFrom:
             fieldRef:
@@ -1921,6 +1928,8 @@ spec:
           value: 10000ms
         - name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE
           value: 10000ms
+        - name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION
+          value: "25,443,587,3306,5432,11211"
         - name: _pod_ns
           valueFrom:
             fieldRef:
@@ -2175,6 +2184,8 @@ spec:
           value: 10000ms
         - name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE
           value: 10000ms
+        - name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION
+          value: "25,443,587,3306,5432,11211"
         - name: _pod_ns
           valueFrom:
             fieldRef:
@@ -2302,7 +2313,7 @@ spec:
 ---
 apiVersion: v1
 data:
-  linkerd-config-overrides: Y2xpVmVyc2lvbjogQ2xpVmVyc2lvbgpjbHVzdGVyTmV0d29ya3M6IENsdXN0ZXJOZXR3b3Jrcwpjb250cm9sUGxhbmVUcmFjaW5nTmFtZXNwYWNlOiAiIgpjb250cm9sbGVySW1hZ2U6IENvbnRyb2xsZXJJbWFnZQpjb250cm9sbGVySW1hZ2VWZXJzaW9uOiBDb250cm9sbGVySW1hZ2VWZXJzaW9uCmNvbnRyb2xsZXJMb2dGb3JtYXQ6IENvbnRyb2xsZXJMb2dGb3JtYXQKY29udHJvbGxlckxvZ0xldmVsOiBDb250cm9sbGVyTG9nTGV2ZWwKZGVidWdDb250YWluZXI6CiAgaW1hZ2U6CiAgICBuYW1lOiBEZWJ1Z0ltYWdlTmFtZQogICAgcHVsbFBvbGljeTogRGVidWdJbWFnZVB1bGxQb2xpY3kKICAgIHZlcnNpb246IERlYnVnVmVyc2lvbgpoZWFydGJlYXRTY2hlZHVsZTogIiIKaWRlbnRpdHk6CiAgaXNzdWVyOgogICAgY3J0RXhwaXJ5OiAiMjAzMC0wOC0yNlQwNzoxMzo0N1oiCiAgICB0bHM6CiAgICAgIGNydFBFTTogfAogICAgICAgIC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQogICAgICAgIE1JSUJ3RENDQVdlZ0F3SUJBZ0lSQUpSSWdaOFJ0TzhFd2cxWGVwZjhUNDR3Q2dZSUtvWkl6ajBFQXdJd0tURW4KICAgICAgICBNQ1VHQTFVRUF4TWVhV1JsYm5ScGRIa3ViR2x1YTJWeVpDNWpiSFZ6ZEdWeUxteHZZMkZzTUI0WERUSXdNRGd5CiAgICAgICAgT0RBM01UTTBOMW9YRFRNd01EZ3lOakEzTVRNME4xb3dLVEVuTUNVR0ExVUVBeE1lYVdSbGJuUnBkSGt1YkdsdQogICAgICAgIGEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTEvRnAKICAgICAgICBmY1JuRGNlZEw2QWpVYVhZUHY0RElNQmFKdWZPSTVOV3R5K1hTWDdKalhnWnRNNzJkUXZSYVlhbnV4RDM2RHQxCiAgICAgICAgMi9KeHlpU2d4S1dSZG9heSthTndNRzR3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQgogICAgICAgIEFmOENBUUF3SFFZRFZSME9CQllFRkkxV25ycU1ZS2FISE9vK3pweWlpRHEycE8wS01Da0dBMVVkRVFRaU1DQ0MKICAgICAgICBIbWxrWlc1MGFYUjVMbXhwYm10bGNtUXVZMngxYzNSbGNpNXNiMk5oYkRBS0JnZ3Foa2pPUFFRREFnTkhBREJFCiAgICAgICAgQWlBdHVvSTVYdUN0ckdWUnpTbVJUbDJyYTI4YVY5TXlUVTdkNXFuVEFGSEtTZ0lnUktDdmx1T1NnQTVPMjFwNQogICAgICAgIDUxdGRybWtIRVpScjBxbExTSmRIWWdFZk16az0KICAgICAgICAtLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCiAgICAgIGtleVBFTTogfAogICAgICAgIC0tLS0tQkVHSU4gRUMgUFJJVkFURSBLRVktLS0tLQogICAgICAgIE1IY0NBUUVFSUFBZThuZmJ6WnU5Yy9PQjIrOHhKTTBGejdOVXdUUWF6dWxrRk5zNFRJNStvQW9HQ0NxR1NNNDkKICAgICAgICBBd0VIb1VRRFFnQUUxL0ZwZmNSbkRjZWRMNkFqVWFYWVB2NERJTUJhSnVmT0k1Tld0eStYU1g3SmpYZ1p0TTcyCiAgICAgICAgZFF2UmFZYW51eEQzNkR0MTIvSnh5aVNneEtXUmRvYXkrUT09CiAgICAgICAgLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQppZGVudGl0eVRydXN0QW5jaG9yc1BFTTogfAogIC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQogIE1JSUJ3VENDQVdhZ0F3SUJBZ0lRZURacDVsRGFJeWdRNVVmTUtackZBVEFLQmdncWhrak9QUVFEQWpBcE1TY3cKICBKUVlEVlFRREV4NXBaR1Z1ZEdsMGVTNXNhVzVyWlhKa0xtTnNkWE4wWlhJdWJHOWpZV3d3SGhjTk1qQXdPREk0CiAgTURjeE1qUTNXaGNOTXpBd09ESTJNRGN4TWpRM1dqQXBNU2N3SlFZRFZRUURFeDVwWkdWdWRHbDBlUzVzYVc1cgogIFpYSmtMbU5zZFhOMFpYSXViRzlqWVd3d1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFScWM3MFoKICBsMXZndzc5cmpCNXVTSVRJQ1VBNkd5ZnZTRmZjdUlpczdCL1hGU2trd0FIVTVTL3MxQUFQK1IwVFg3SEJXVUM0CiAgdWFHNFdXc2l3SktObjdtZ28zQXdiakFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQgogIC93SUJBVEFkQmdOVkhRNEVGZ1FVNVl0alZWUGZkN0k3TkxIc24yQzI2RUJ5R1Ywd0tRWURWUjBSQkNJd0lJSWUKICBhV1JsYm5ScGRIa3ViR2x1YTJWeVpDNWpiSFZ6ZEdWeUxteHZZMkZzTUFvR0NDcUdTTTQ5QkFNQ0Ewa0FNRVlDCiAgSVFDTjdsQkZMRER2ang2VjArWGtqcEtFUlJzSllmNWFkTXZubG9GbDQ4aWxKZ0loQU50eGhuZGNyK1FKUHVDOAogIHZnVUMwZDIvOUZNdWVJVk1iKzQ2V1RDT2pzcXIKICAtLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCmltYWdlUHVsbFBvbGljeTogSW1hZ2VQdWxsUG9saWN5CmltYWdlUHVsbFNlY3JldHM6IG51bGwKbGlua2VyZFZlcnNpb246ICIiCnByb2ZpbGVWYWxpZGF0b3I6CiAgY2FCdW5kbGU6IHByb2ZpbGUgdmFsaWRhdG9yIENBIGJ1bmRsZQogIGNydFBFTTogcHJvZmlsZSB2YWxpZGF0b3IgY3J0CiAga2V5UEVNOiBwcm9maWxlIHZhbGlkYXRvciBrZXkKcHJveHk6CiAgaW1hZ2U6CiAgICBuYW1lOiBQcm94eUltYWdlTmFtZQogICAgcHVsbFBvbGljeTogSW1hZ2VQdWxsUG9saWN5CiAgICB2ZXJzaW9uOiBQcm94eVZlcnNpb24KICBpbmJvdW5kQ29ubmVjdFRpbWVvdXQ6ICIiCiAgb3BhcXVlUG9ydHM6ICIiCiAgb3V0Ym91bmRDb25uZWN0VGltZW91dDogIiIKICByZXNvdXJjZXM6CiAgICBjcHU6CiAgICAgIGxpbWl0OiBjcHUtbGltaXQKICAgICAgcmVxdWVzdDogY3B1LXJlcXVlc3QKICAgIG1lbW9yeToKICAgICAgbGltaXQ6IG1lbW9yeS1saW1pdAogICAgICByZXF1ZXN0OiBtZW1vcnktcmVxdWVzdApwcm94eUNvbnRhaW5lck5hbWU6IFByb3h5Q29udGFpbmVyTmFtZQpwcm94eUluaXQ6CiAgaWdub3JlT3V0Ym91bmRQb3J0czogIjQ0MyIKICBpbWFnZToKICAgIG5hbWU6IFByb3h5SW5pdEltYWdlTmFtZQogICAgcHVsbFBvbGljeTogSW1hZ2VQdWxsUG9saWN5CiAgICB2ZXJzaW9uOiBQcm94eUluaXRWZXJzaW9uCnByb3h5SW5qZWN0b3I6CiAgY2FCdW5kbGU6IHByb3h5IGluamVjdG9yIENBIGJ1bmRsZQogIGNydFBFTTogcHJveHkgaW5qZWN0b3IgY3J0CiAga2V5UEVNOiBwcm94eSBpbmplY3RvciBrZXkKd2ViaG9va0ZhaWx1cmVQb2xpY3k6IFdlYmhvb2tGYWlsdXJlUG9saWN5Cg==
+  linkerd-config-overrides: Y2xpVmVyc2lvbjogQ2xpVmVyc2lvbgpjbHVzdGVyTmV0d29ya3M6IENsdXN0ZXJOZXR3b3Jrcwpjb250cm9sUGxhbmVUcmFjaW5nTmFtZXNwYWNlOiAiIgpjb250cm9sbGVySW1hZ2U6IENvbnRyb2xsZXJJbWFnZQpjb250cm9sbGVySW1hZ2VWZXJzaW9uOiBDb250cm9sbGVySW1hZ2VWZXJzaW9uCmNvbnRyb2xsZXJMb2dGb3JtYXQ6IENvbnRyb2xsZXJMb2dGb3JtYXQKY29udHJvbGxlckxvZ0xldmVsOiBDb250cm9sbGVyTG9nTGV2ZWwKZGVidWdDb250YWluZXI6CiAgaW1hZ2U6CiAgICBuYW1lOiBEZWJ1Z0ltYWdlTmFtZQogICAgcHVsbFBvbGljeTogRGVidWdJbWFnZVB1bGxQb2xpY3kKICAgIHZlcnNpb246IERlYnVnVmVyc2lvbgpoZWFydGJlYXRTY2hlZHVsZTogIiIKaWRlbnRpdHk6CiAgaXNzdWVyOgogICAgY3J0RXhwaXJ5OiAiMjAzMC0wOC0yNlQwNzoxMzo0N1oiCiAgICB0bHM6CiAgICAgIGNydFBFTTogfAogICAgICAgIC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQogICAgICAgIE1JSUJ3RENDQVdlZ0F3SUJBZ0lSQUpSSWdaOFJ0TzhFd2cxWGVwZjhUNDR3Q2dZSUtvWkl6ajBFQXdJd0tURW4KICAgICAgICBNQ1VHQTFVRUF4TWVhV1JsYm5ScGRIa3ViR2x1YTJWeVpDNWpiSFZ6ZEdWeUxteHZZMkZzTUI0WERUSXdNRGd5CiAgICAgICAgT0RBM01UTTBOMW9YRFRNd01EZ3lOakEzTVRNME4xb3dLVEVuTUNVR0ExVUVBeE1lYVdSbGJuUnBkSGt1YkdsdQogICAgICAgIGEyVnlaQzVqYkhWemRHVnlMbXh2WTJGc01Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTEvRnAKICAgICAgICBmY1JuRGNlZEw2QWpVYVhZUHY0RElNQmFKdWZPSTVOV3R5K1hTWDdKalhnWnRNNzJkUXZSYVlhbnV4RDM2RHQxCiAgICAgICAgMi9KeHlpU2d4S1dSZG9heSthTndNRzR3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQgogICAgICAgIEFmOENBUUF3SFFZRFZSME9CQllFRkkxV25ycU1ZS2FISE9vK3pweWlpRHEycE8wS01Da0dBMVVkRVFRaU1DQ0MKICAgICAgICBIbWxrWlc1MGFYUjVMbXhwYm10bGNtUXVZMngxYzNSbGNpNXNiMk5oYkRBS0JnZ3Foa2pPUFFRREFnTkhBREJFCiAgICAgICAgQWlBdHVvSTVYdUN0ckdWUnpTbVJUbDJyYTI4YVY5TXlUVTdkNXFuVEFGSEtTZ0lnUktDdmx1T1NnQTVPMjFwNQogICAgICAgIDUxdGRybWtIRVpScjBxbExTSmRIWWdFZk16az0KICAgICAgICAtLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCiAgICAgIGtleVBFTTogfAogICAgICAgIC0tLS0tQkVHSU4gRUMgUFJJVkFURSBLRVktLS0tLQogICAgICAgIE1IY0NBUUVFSUFBZThuZmJ6WnU5Yy9PQjIrOHhKTTBGejdOVXdUUWF6dWxrRk5zNFRJNStvQW9HQ0NxR1NNNDkKICAgICAgICBBd0VIb1VRRFFnQUUxL0ZwZmNSbkRjZWRMNkFqVWFYWVB2NERJTUJhSnVmT0k1Tld0eStYU1g3SmpYZ1p0TTcyCiAgICAgICAgZFF2UmFZYW51eEQzNkR0MTIvSnh5aVNneEtXUmRvYXkrUT09CiAgICAgICAgLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQppZGVudGl0eVRydXN0QW5jaG9yc1BFTTogfAogIC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQogIE1JSUJ3VENDQVdhZ0F3SUJBZ0lRZURacDVsRGFJeWdRNVVmTUtackZBVEFLQmdncWhrak9QUVFEQWpBcE1TY3cKICBKUVlEVlFRREV4NXBaR1Z1ZEdsMGVTNXNhVzVyWlhKa0xtTnNkWE4wWlhJdWJHOWpZV3d3SGhjTk1qQXdPREk0CiAgTURjeE1qUTNXaGNOTXpBd09ESTJNRGN4TWpRM1dqQXBNU2N3SlFZRFZRUURFeDVwWkdWdWRHbDBlUzVzYVc1cgogIFpYSmtMbU5zZFhOMFpYSXViRzlqWVd3d1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFScWM3MFoKICBsMXZndzc5cmpCNXVTSVRJQ1VBNkd5ZnZTRmZjdUlpczdCL1hGU2trd0FIVTVTL3MxQUFQK1IwVFg3SEJXVUM0CiAgdWFHNFdXc2l3SktObjdtZ28zQXdiakFPQmdOVkhROEJBZjhFQkFNQ0FRWXdFZ1lEVlIwVEFRSC9CQWd3QmdFQgogIC93SUJBVEFkQmdOVkhRNEVGZ1FVNVl0alZWUGZkN0k3TkxIc24yQzI2RUJ5R1Ywd0tRWURWUjBSQkNJd0lJSWUKICBhV1JsYm5ScGRIa3ViR2x1YTJWeVpDNWpiSFZ6ZEdWeUxteHZZMkZzTUFvR0NDcUdTTTQ5QkFNQ0Ewa0FNRVlDCiAgSVFDTjdsQkZMRER2ang2VjArWGtqcEtFUlJzSllmNWFkTXZubG9GbDQ4aWxKZ0loQU50eGhuZGNyK1FKUHVDOAogIHZnVUMwZDIvOUZNdWVJVk1iKzQ2V1RDT2pzcXIKICAtLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCmltYWdlUHVsbFBvbGljeTogSW1hZ2VQdWxsUG9saWN5CmltYWdlUHVsbFNlY3JldHM6IG51bGwKbGlua2VyZFZlcnNpb246ICIiCnByb2ZpbGVWYWxpZGF0b3I6CiAgY2FCdW5kbGU6IHByb2ZpbGUgdmFsaWRhdG9yIENBIGJ1bmRsZQogIGNydFBFTTogcHJvZmlsZSB2YWxpZGF0b3IgY3J0CiAga2V5UEVNOiBwcm9maWxlIHZhbGlkYXRvciBrZXkKcHJveHk6CiAgaW1hZ2U6CiAgICBuYW1lOiBQcm94eUltYWdlTmFtZQogICAgcHVsbFBvbGljeTogSW1hZ2VQdWxsUG9saWN5CiAgICB2ZXJzaW9uOiBQcm94eVZlcnNpb24KICBpbmJvdW5kQ29ubmVjdFRpbWVvdXQ6ICIiCiAgb3V0Ym91bmRDb25uZWN0VGltZW91dDogIiIKICByZXNvdXJjZXM6CiAgICBjcHU6CiAgICAgIGxpbWl0OiBjcHUtbGltaXQKICAgICAgcmVxdWVzdDogY3B1LXJlcXVlc3QKICAgIG1lbW9yeToKICAgICAgbGltaXQ6IG1lbW9yeS1saW1pdAogICAgICByZXF1ZXN0OiBtZW1vcnktcmVxdWVzdApwcm94eUNvbnRhaW5lck5hbWU6IFByb3h5Q29udGFpbmVyTmFtZQpwcm94eUluaXQ6CiAgaWdub3JlT3V0Ym91bmRQb3J0czogIjQ0MyIKICBpbWFnZToKICAgIG5hbWU6IFByb3h5SW5pdEltYWdlTmFtZQogICAgcHVsbFBvbGljeTogSW1hZ2VQdWxsUG9saWN5CiAgICB2ZXJzaW9uOiBQcm94eUluaXRWZXJzaW9uCnByb3h5SW5qZWN0b3I6CiAgY2FCdW5kbGU6IHByb3h5IGluamVjdG9yIENBIGJ1bmRsZQogIGNydFBFTTogcHJveHkgaW5qZWN0b3IgY3J0CiAga2V5UEVNOiBwcm94eSBpbmplY3RvciBrZXkKd2ViaG9va0ZhaWx1cmVQb2xpY3k6IFdlYmhvb2tGYWlsdXJlUG9saWN5Cg==
 kind: Secret
 metadata:
   creationTimestamp: null
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index bd162b8f6..787e1eeb0 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -1617,6 +1617,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=cluster.local
         - -identity-trust-domain=cluster.local
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index 92464eb57..128ceb248 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -1603,6 +1603,7 @@ spec:
         - -enable-endpoint-slices=false
         - -cluster-domain=example.com
         - -identity-trust-domain=example.com
+        - -default-opaque-ports=25,443,587,3306,5432,11211
         image: cr.l5d.io/linkerd/controller:install-control-plane-version
         imagePullPolicy: IfNotPresent
         livenessProbe:
diff --git a/controller/api/destination/endpoint_translator_test.go b/controller/api/destination/endpoint_translator_test.go
index 77be5e817..24a5b74dc 100644
--- a/controller/api/destination/endpoint_translator_test.go
+++ b/controller/api/destination/endpoint_translator_test.go
@@ -146,6 +146,7 @@ metadata:
 		true,
 		"service-name.service-ns",
 		"test-123",
+		map[uint32]struct{}{},
 		k8sAPI.Node(),
 		mockGetServer,
 		logging.WithField("test", t.Name()),
diff --git a/controller/api/destination/server_test.go b/controller/api/destination/server_test.go
index e5d994151..d94f94a50 100644
--- a/controller/api/destination/server_test.go
+++ b/controller/api/destination/server_test.go
@@ -217,9 +217,17 @@ metadata:
 		t.Fatalf("NewFakeAPI returned an error: %s", err)
 	}
 	log := logging.WithField("test", t.Name())
+	defaultOpaquePorts := map[uint32]struct{}{
+		25:    {},
+		443:   {},
+		587:   {},
+		3306:  {},
+		5432:  {},
+		11211: {},
+	}
 
 	endpoints := watcher.NewEndpointsWatcher(k8sAPI, log, false)
-	opaquePorts := watcher.NewOpaquePortsWatcher(k8sAPI, log)
+	opaquePorts := watcher.NewOpaquePortsWatcher(k8sAPI, log, defaultOpaquePorts)
 	profiles := watcher.NewProfileWatcher(k8sAPI, log)
 	trafficSplits := watcher.NewTrafficSplitWatcher(k8sAPI, log)
 	ips := watcher.NewIPWatcher(k8sAPI, endpoints, log)
@@ -239,6 +247,7 @@ metadata:
 		"linkerd",
 		"trust.domain",
 		"mycluster.local",
+		defaultOpaquePorts,
 		k8sAPI,
 		log,
 		make(<-chan struct{}),
diff --git a/controller/api/destination/watcher/opaque_ports_watcher_test.go b/controller/api/destination/watcher/opaque_ports_watcher_test.go
index 30dfab373..caa165c62 100644
--- a/controller/api/destination/watcher/opaque_ports_watcher_test.go
+++ b/controller/api/destination/watcher/opaque_ports_watcher_test.go
@@ -88,6 +88,15 @@ func (bopl *testOpaquePortsListener) UpdateService(ports map[uint32]struct{}) {
 }
 
 func TestOpaquePortsWatcher(t *testing.T) {
+	defaultOpaquePorts := map[uint32]struct{}{
+		25:    {},
+		443:   {},
+		587:   {},
+		3306:  {},
+		5432:  {},
+		11211: {},
+	}
+
 	for _, tt := range []struct {
 		name                string
 		initialState        []string
@@ -176,7 +185,7 @@ func TestOpaquePortsWatcher(t *testing.T) {
 		if err != nil {
 			t.Fatalf("NewFakeAPI returned an error: %s", err)
 		}
-		watcher := NewOpaquePortsWatcher(k8sAPI, logging.WithField("test", t.Name()))
+		watcher := NewOpaquePortsWatcher(k8sAPI, logging.WithField("test", t.Name()), defaultOpaquePorts)
 		k8sAPI.Sync(nil)
 		listener := newTestOpaquePortsListener()
 		watcher.Subscribe(tt.service, listener)
diff --git a/pkg/util/parsing_test.go b/pkg/util/parsing_test.go
new file mode 100644
index 000000000..ee9c365d3
--- /dev/null
+++ b/pkg/util/parsing_test.go
@@ -0,0 +1,57 @@
+package util
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestParsePorts(t *testing.T) {
+	testCases := []struct {
+		ports  string
+		result map[uint32]struct{}
+	}{
+		{
+			"25,443,587,3306,5432,11211",
+			map[uint32]struct{}{
+				25:    {},
+				443:   {},
+				587:   {},
+				3306:  {},
+				5432:  {},
+				11211: {},
+			},
+		},
+		{
+			"25,443-447,3306,5432-5435,11211",
+			map[uint32]struct{}{
+				25:    {},
+				443:   {},
+				444:   {},
+				445:   {},
+				446:   {},
+				447:   {},
+				3306:  {},
+				5432:  {},
+				5433:  {},
+				5434:  {},
+				5435:  {},
+				11211: {},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc // pin
+		t.Run(fmt.Sprintf("test %s", tc.ports), func(t *testing.T) {
+			ports, err := ParsePorts(tc.ports)
+			if err != nil {
+				t.Fatalf("could not parse ports: %v", err)
+			}
+
+			if !reflect.DeepEqual(ports, tc.result) {
+				t.Fatalf("Expected output: \"%v\", got: \"%v\"", tc.result, ports)
+			}
+		})
+	}
+}
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index 180e0cc2a..610241650 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -342,7 +342,17 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 		}
 
 		// apply stage 1
-		out, err = TestHelper.KubectlApply(out, "")
+		// Limit the pruning only to known resources
+		// that we intend to be delete in this stage to prevent it
+		// from deleting other resources that have the
+		// label
+		out, err = TestHelper.KubectlApplyWithArgs(out, []string{
+			"--prune",
+			"-l", "linkerd.io/control-plane-ns=linkerd",
+			"--prune-whitelist", "rbac.authorization.k8s.io/v1/clusterrole",
+			"--prune-whitelist", "rbac.authorization.k8s.io/v1/clusterrolebinding",
+			"--prune-whitelist", "apiregistration.k8s.io/v1/apiservice",
+		}...)
 		if err != nil {
 			testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
 				"kubectl apply command failed\n%s", out)
@@ -399,20 +409,23 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 		}
 	}
 
-	out, err = TestHelper.KubectlApply(out, "")
+	// Limit the pruning only to known resources
+	// that we intend to be delete in this stage to prevent it
+	// from deleting other resources that have the
+	// label
+	out, err = TestHelper.KubectlApplyWithArgs(out, []string{
+		"--prune",
+		"-l", "linkerd.io/control-plane-ns=linkerd",
+		"--prune-whitelist", "apps/v1/deployment",
+		"--prune-whitelist", "core/v1/service",
+		"--prune-whitelist", "core/v1/configmap",
+	}...)
 	if err != nil {
 		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
 			"'kubectl apply' command failed\n%s", out)
 	}
 
-	// Wait for the proxy injector to be up
-	name := "linkerd-proxy-injector"
-	ns := "linkerd"
-	o, err := TestHelper.Kubectl("", "--namespace="+ns, "rollout", "status", "--timeout=120s", "deploy/"+name)
-	if err != nil {
-		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to wait for condition=available for deploy/%s in namespace %s", name, ns),
-			"failed to wait for condition=available for deploy/%s in namespace %s: %s: %s", name, ns, err, o)
-	}
+	TestHelper.WaitRollout(t)
 
 	if TestHelper.ExternalPrometheus() {
 
@@ -466,15 +479,12 @@ func helmOverridesEdge(root *tls.CA) []string {
 	return []string{
 		"--set", "controllerLogLevel=debug",
 		"--set", "linkerdVersion=" + TestHelper.GetVersion(),
-		"--set", "proxy.image.version=" + TestHelper.GetVersion(),
 		// these ports will get verified in test/integration/inject
 		"--set", "proxyInit.ignoreInboundPorts=" + skippedInboundPortsEscaped,
-		"--set", "identityTrustDomain=cluster.local",
 		"--set", "identityTrustAnchorsPEM=" + root.Cred.Crt.EncodeCertificatePEM(),
 		"--set", "identity.issuer.tls.crtPEM=" + root.Cred.Crt.EncodeCertificatePEM(),
 		"--set", "identity.issuer.tls.keyPEM=" + root.Cred.EncodePrivateKeyPEM(),
 		"--set", "identity.issuer.crtExpiry=" + root.Cred.Crt.Certificate.NotAfter.Format(time.RFC3339),
-		"--set", "grafana.image.version=" + TestHelper.GetVersion(),
 	}
 }
 
@@ -507,25 +517,13 @@ func TestInstallHelm(t *testing.T) {
 			"'helm install' command failed\n%s\n%s", stdout, stderr)
 	}
 
-	// Wait for the proxy injector to be up
-	name := "linkerd-proxy-injector"
-	ns := "linkerd"
-	o, err := TestHelper.Kubectl("", "--namespace="+ns, "wait", "--for=condition=available", "--timeout=120s", "deploy/"+name)
-	if err != nil {
-		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to wait for condition=available for deploy/%s in namespace %s", name, ns),
-			"failed to wait for condition=available for deploy/%s in namespace %s: %s: %s", name, ns, err, o)
-	}
+	TestHelper.WaitRollout(t)
 
 	if TestHelper.UpgradeHelmFromVersion() == "" {
 		vizChart := TestHelper.GetLinkerdVizHelmChart()
 		vizArgs := []string{
 			"--set", "linkerdVersion=" + TestHelper.GetVersion(),
 			"--set", "namespace=" + TestHelper.GetVizNamespace(),
-			"--set", "dashboard.image.tag=" + TestHelper.GetVersion(),
-			"--set", "grafana.image.tag=" + TestHelper.GetVersion(),
-			"--set", "tap.image.tag=" + TestHelper.GetVersion(),
-			"--set", "metricsAPI.image.tag=" + TestHelper.GetVersion(),
-			"--set", "tapInjector.image.tag=" + TestHelper.GetVersion(),
 		}
 		// Install Viz Extension Chart
 		if stdout, stderr, err := TestHelper.HelmInstallPlain(vizChart, "l5d-viz", vizArgs...); err != nil {
@@ -555,7 +553,6 @@ func TestInstallMulticluster(t *testing.T) {
 	if TestHelper.GetMulticlusterHelmReleaseName() != "" {
 		flags := []string{
 			"--set", "linkerdVersion=" + TestHelper.GetVersion(),
-			"--set", "controllerImageVersion=" + TestHelper.GetVersion(),
 		}
 		if stdout, stderr, err := TestHelper.HelmInstallMulticluster(TestHelper.GetMulticlusterHelmChart(), flags...); err != nil {
 			testutil.AnnotatedFatalf(t, "'helm install' command failed",
@@ -639,11 +636,6 @@ func TestUpgradeHelm(t *testing.T) {
 	vizArgs := []string{
 		"--set", "linkerdVersion=" + TestHelper.GetVersion(),
 		"--set", "namespace=" + TestHelper.GetVizNamespace(),
-		"--set", "dashboard.image.tag=" + TestHelper.GetVersion(),
-		"--set", "grafana.image.tag=" + TestHelper.GetVersion(),
-		"--set", "tap.image.tag=" + TestHelper.GetVersion(),
-		"--set", "metricsAPI.image.tag=" + TestHelper.GetVersion(),
-		"--set", "tapInjector.image.tag=" + TestHelper.GetVersion(),
 		"--wait",
 	}
 	// Install Viz Extension Chart
diff --git a/test/integration/testdata/check.cni.golden b/test/integration/testdata/check.cni.golden
index 3ce7a52a1..6c48b470c 100644
--- a/test/integration/testdata/check.cni.golden
+++ b/test/integration/testdata/check.cni.golden
@@ -15,8 +15,6 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ controller pod is running
-√ can initialize the client
-√ can query the control plane API
 
 linkerd-config
 --------------
@@ -62,6 +60,8 @@ linkerd-webhooks-and-apisvc-tls
 linkerd-api
 -----------
 √ control plane pods are ready
+√ can initialize the client
+√ can query the control plane API
 
 linkerd-version
 ---------------
diff --git a/test/integration/testdata/check.cni.proxy.golden b/test/integration/testdata/check.cni.proxy.golden
index 6b3ea8f1b..67cd94108 100644
--- a/test/integration/testdata/check.cni.proxy.golden
+++ b/test/integration/testdata/check.cni.proxy.golden
@@ -15,8 +15,6 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ controller pod is running
-√ can initialize the client
-√ can query the control plane API
 
 linkerd-config
 --------------
@@ -66,6 +64,8 @@ linkerd-identity-data-plane
 linkerd-api
 -----------
 √ control plane pods are ready
+√ can initialize the client
+√ can query the control plane API
 
 linkerd-version
 ---------------
diff --git a/test/integration/testdata/check.golden b/test/integration/testdata/check.golden
index eb786650a..ed2180f7e 100644
--- a/test/integration/testdata/check.golden
+++ b/test/integration/testdata/check.golden
@@ -15,8 +15,6 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ controller pod is running
-√ can initialize the client
-√ can query the control plane API
 
 linkerd-config
 --------------
@@ -50,6 +48,8 @@ linkerd-webhooks-and-apisvc-tls
 linkerd-api
 -----------
 √ control plane pods are ready
+√ can initialize the client
+√ can query the control plane API
 
 linkerd-version
 ---------------
diff --git a/test/integration/testdata/check.multicluster.proxy.golden b/test/integration/testdata/check.multicluster.proxy.golden
index 9f57563fe..e97f8263e 100644
--- a/test/integration/testdata/check.multicluster.proxy.golden
+++ b/test/integration/testdata/check.multicluster.proxy.golden
@@ -15,8 +15,6 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ controller pod is running
-√ can initialize the client
-√ can query the control plane API
 
 linkerd-config
 --------------
@@ -54,6 +52,8 @@ linkerd-identity-data-plane
 linkerd-api
 -----------
 √ control plane pods are ready
+√ can initialize the client
+√ can query the control plane API
 
 linkerd-version
 ---------------
diff --git a/test/integration/testdata/check.proxy.golden b/test/integration/testdata/check.proxy.golden
index 5e4be3128..c1311d9f8 100644
--- a/test/integration/testdata/check.proxy.golden
+++ b/test/integration/testdata/check.proxy.golden
@@ -15,8 +15,6 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ controller pod is running
-√ can initialize the client
-√ can query the control plane API
 
 linkerd-config
 --------------
@@ -54,6 +52,8 @@ linkerd-identity-data-plane
 linkerd-api
 -----------
 √ control plane pods are ready
+√ can initialize the client
+√ can query the control plane API
 
 linkerd-version
 ---------------
diff --git a/test/integration/testdata/inject.report.golden b/test/integration/testdata/inject.report.golden
index c7c5928fd..7031afeea 100644
--- a/test/integration/testdata/inject.report.golden
+++ b/test/integration/testdata/inject.report.golden
@@ -1,6 +1,6 @@
 
 deployment "smoke-test-terminus" injected
-service "smoke-test-terminus-svc" skipped
+service "smoke-test-terminus-svc" injected
 deployment "smoke-test-gateway" injected
-service "smoke-test-gateway-svc" skipped
+service "smoke-test-gateway-svc" injected
 
diff --git a/test/integration/tracing/testdata/check.jaeger.golden b/test/integration/tracing/testdata/check.jaeger.golden
index d422b8935..a9d18b29a 100644
--- a/test/integration/tracing/testdata/check.jaeger.golden
+++ b/test/integration/tracing/testdata/check.jaeger.golden
@@ -4,8 +4,6 @@ linkerd-jaeger
 √ collector and jaeger service account exists
 √ collector config map exists
 √ jaeger extension pods are injected
-√ collector pod is running
-√ jaeger pod is running
-√ jaeger injector pod is running
+√ jaeger extension pods are running
 
 Status check results are √
diff --git a/test/integration/tracing/tracing_test.go b/test/integration/tracing/tracing_test.go
index 6587093ba..ddb19ff59 100644
--- a/test/integration/tracing/tracing_test.go
+++ b/test/integration/tracing/tracing_test.go
@@ -42,12 +42,23 @@ func TestMain(m *testing.M) {
 //////////////////////
 
 func TestTracing(t *testing.T) {
+	t.Skip(`Temporarily skipped while we figure why it keeps on failing, only in CI ¯\_(ツ)_/¯`)
 
 	ctx := context.Background()
 	if os.Getenv("RUN_ARM_TEST") != "" {
 		t.Skip("Skipped. Jaeger & Open Census images does not support ARM yet")
 	}
 
+	// cleanup cluster before proceeding
+	namespaces := []string{"smoke-test", "smoke-test-manual", "smoke-test-ann", "opaque-ports-test"}
+	for _, ns := range namespaces {
+		prefixedNs := TestHelper.GetTestNamespace(ns)
+		if err := TestHelper.DeleteNamespaceIfExists(ctx, prefixedNs); err != nil {
+			testutil.AnnotatedFatalf(t, "error deleting namespace",
+				"error deleting namespace '%s': %s", prefixedNs, err)
+		}
+	}
+
 	// linkerd-jaeger extension
 	tracingNs := "linkerd-jaeger"
 	out, err := TestHelper.LinkerdRun("jaeger", "install")
@@ -68,7 +79,7 @@ func TestTracing(t *testing.T) {
 	err = TestHelper.RetryFor(timeout, func() error {
 		out, err := TestHelper.LinkerdRun(checkCmd...)
 		if err != nil {
-			return fmt.Errorf("'linkerd jaeger check' command failed\n%s", err)
+			return fmt.Errorf("'linkerd jaeger check' command failed\n%s\n%s", err, out)
 		}
 		err = TestHelper.ValidateOutput(out, golden)
 		if err != nil {
diff --git a/testutil/kubernetes_helper.go b/testutil/kubernetes_helper.go
index e9d7a2e0c..ebd961423 100644
--- a/testutil/kubernetes_helper.go
+++ b/testutil/kubernetes_helper.go
@@ -7,6 +7,7 @@ import (
 	"os/exec"
 	"regexp"
 	"strings"
+	"testing"
 	"time"
 
 	"github.com/linkerd/linkerd2/pkg/k8s"
@@ -96,7 +97,9 @@ func (h *KubernetesHelper) createNamespaceIfNotExists(ctx context.Context, names
 	return nil
 }
 
-func (h *KubernetesHelper) deleteNamespaceIfExists(ctx context.Context, namespace string) error {
+// DeleteNamespaceIfExists attempts to delete the given namespace,
+// using the K8s API directly
+func (h *KubernetesHelper) DeleteNamespaceIfExists(ctx context.Context, namespace string) error {
 	err := h.clientset.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{})
 
 	if err != nil && !kerrors.IsNotFound(err) {
@@ -130,6 +133,15 @@ func (h *KubernetesHelper) KubectlApply(stdin string, namespace string) (string,
 	return h.Kubectl(stdin, args...)
 }
 
+// KubectlApplyWithArgs applies a given configuration string with the passed
+// flags
+func (h *KubernetesHelper) KubectlApplyWithArgs(stdin string, cmdArgs ...string) (string, error) {
+	args := []string{"apply"}
+	args = append(args, cmdArgs...)
+	args = append(args, "-f", "-")
+	return h.Kubectl(stdin, args...)
+}
+
 // Kubectl executes an arbitrary Kubectl command
 func (h *KubernetesHelper) Kubectl(stdin string, arg ...string) (string, error) {
 	withContext := append([]string{"--context=" + h.k8sContext}, arg...)
@@ -337,3 +349,18 @@ func (h *KubernetesHelper) URLFor(ctx context.Context, namespace, deployName str
 
 	return pf.URLFor(""), nil
 }
+
+// WaitRollout blocks until all the deployments in the linkerd namespace have been
+// completely rolled out (and their pods are ready)
+func (h *KubernetesHelper) WaitRollout(t *testing.T) {
+	for deploy, deploySpec := range LinkerdDeployReplicasEdge {
+		if deploySpec.Namespace == "linkerd" {
+			o, err := h.Kubectl("", "--namespace=linkerd", "rollout", "status", "--timeout=120s", "deploy/"+deploy)
+			if err != nil {
+				AnnotatedFatalf(t,
+					fmt.Sprintf("failed to wait rollout of deploy/%s", deploy),
+					"failed to wait for rollout of deploy/%s: %s: %s", deploy, err, o)
+			}
+		}
+	}
+}
diff --git a/testutil/test_helper.go b/testutil/test_helper.go
index 1675a281b..4effcc622 100644
--- a/testutil/test_helper.go
+++ b/testutil/test_helper.go
@@ -642,7 +642,7 @@ func (h *TestHelper) WithDataPlaneNamespace(ctx context.Context, testName string
 			"failed to create %s namespace: %s", prefixedNs, err)
 	}
 	test(t, prefixedNs)
-	if err := h.deleteNamespaceIfExists(ctx, prefixedNs); err != nil {
+	if err := h.DeleteNamespaceIfExists(ctx, prefixedNs); err != nil {
 		AnnotatedFatalf(t, fmt.Sprintf("failed to delete %s namespace", prefixedNs),
 			"failed to delete %s namespace: %s", prefixedNs, err)
 	}
diff --git a/viz/cmd/testdata/install_default.golden b/viz/cmd/testdata/install_default.golden
index 342b4eef7..d3cf5495f 100644
--- a/viz/cmd/testdata/install_default.golden
+++ b/viz/cmd/testdata/install_default.golden
@@ -1038,6 +1038,8 @@ metadata:
   namespace: linkerd-viz
   annotations:
     linkerd.io/created-by: linkerd/helm dev-undefined
+  labels:
+    linkerd.io/extension: viz
 type: kubernetes.io/tls
 data:
   tls.crt: dGVzdC10YXAtY3J0LXBlbQ==
@@ -1083,6 +1085,7 @@ metadata:
 spec:
   type: ClusterIP
   selector:
+    linkerd.io/extension: viz
     component: tap-injector
   ports:
   - name: tap-injector
@@ -1109,7 +1112,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 5cb033e3116205dd502bee6f822717a8efe049d8ada2f02d1343a2c8b0cd3c93
+        checksum/config: 954486b77f49f95fc44392cf8ea7672033f74102bab63103d00a61ea7895c281
         linkerd.io/created-by: linkerd/helm dev-undefined
       labels:
         linkerd.io/extension: viz
diff --git a/viz/cmd/testdata/install_default_overrides.golden b/viz/cmd/testdata/install_default_overrides.golden
index 9e91bc88e..ad04eaa0a 100644
--- a/viz/cmd/testdata/install_default_overrides.golden
+++ b/viz/cmd/testdata/install_default_overrides.golden
@@ -1038,6 +1038,8 @@ metadata:
   namespace: linkerd-viz
   annotations:
     linkerd.io/created-by: linkerd/helm dev-undefined
+  labels:
+    linkerd.io/extension: viz
 type: kubernetes.io/tls
 data:
   tls.crt: dGVzdC10YXAtY3J0LXBlbQ==
@@ -1083,6 +1085,7 @@ metadata:
 spec:
   type: ClusterIP
   selector:
+    linkerd.io/extension: viz
     component: tap-injector
   ports:
   - name: tap-injector
@@ -1109,7 +1112,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 5cb033e3116205dd502bee6f822717a8efe049d8ada2f02d1343a2c8b0cd3c93
+        checksum/config: 954486b77f49f95fc44392cf8ea7672033f74102bab63103d00a61ea7895c281
         linkerd.io/created-by: linkerd/helm dev-undefined
       labels:
         linkerd.io/extension: viz
diff --git a/viz/cmd/testdata/install_grafana_disabled.golden b/viz/cmd/testdata/install_grafana_disabled.golden
index 0527575d0..f3ce547f1 100644
--- a/viz/cmd/testdata/install_grafana_disabled.golden
+++ b/viz/cmd/testdata/install_grafana_disabled.golden
@@ -856,6 +856,8 @@ metadata:
   namespace: linkerd-viz
   annotations:
     linkerd.io/created-by: linkerd/helm dev-undefined
+  labels:
+    linkerd.io/extension: viz
 type: kubernetes.io/tls
 data:
   tls.crt: dGVzdC10YXAtY3J0LXBlbQ==
@@ -901,6 +903,7 @@ metadata:
 spec:
   type: ClusterIP
   selector:
+    linkerd.io/extension: viz
     component: tap-injector
   ports:
   - name: tap-injector
@@ -927,7 +930,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 5cb033e3116205dd502bee6f822717a8efe049d8ada2f02d1343a2c8b0cd3c93
+        checksum/config: 954486b77f49f95fc44392cf8ea7672033f74102bab63103d00a61ea7895c281
         linkerd.io/created-by: linkerd/helm dev-undefined
       labels:
         linkerd.io/extension: viz
diff --git a/viz/cmd/testdata/install_prometheus_disabled.golden b/viz/cmd/testdata/install_prometheus_disabled.golden
index ef88872ea..1db08e335 100644
--- a/viz/cmd/testdata/install_prometheus_disabled.golden
+++ b/viz/cmd/testdata/install_prometheus_disabled.golden
@@ -748,6 +748,8 @@ metadata:
   namespace: linkerd-viz
   annotations:
     linkerd.io/created-by: linkerd/helm dev-undefined
+  labels:
+    linkerd.io/extension: viz
 type: kubernetes.io/tls
 data:
   tls.crt: dGVzdC10YXAtY3J0LXBlbQ==
@@ -793,6 +795,7 @@ metadata:
 spec:
   type: ClusterIP
   selector:
+    linkerd.io/extension: viz
     component: tap-injector
   ports:
   - name: tap-injector
@@ -819,7 +822,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 5cb033e3116205dd502bee6f822717a8efe049d8ada2f02d1343a2c8b0cd3c93
+        checksum/config: 954486b77f49f95fc44392cf8ea7672033f74102bab63103d00a61ea7895c281
         linkerd.io/created-by: linkerd/helm dev-undefined
       labels:
         linkerd.io/extension: viz
diff --git a/viz/cmd/testdata/install_proxy_resources.golden b/viz/cmd/testdata/install_proxy_resources.golden
index 6a8b37b24..5b7d7ee14 100644
--- a/viz/cmd/testdata/install_proxy_resources.golden
+++ b/viz/cmd/testdata/install_proxy_resources.golden
@@ -1050,6 +1050,8 @@ metadata:
   namespace: linkerd-viz
   annotations:
     linkerd.io/created-by: linkerd/helm dev-undefined
+  labels:
+    linkerd.io/extension: viz
 type: kubernetes.io/tls
 data:
   tls.crt: dGVzdC10YXAtY3J0LXBlbQ==
@@ -1095,6 +1097,7 @@ metadata:
 spec:
   type: ClusterIP
   selector:
+    linkerd.io/extension: viz
     component: tap-injector
   ports:
   - name: tap-injector
@@ -1121,7 +1124,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 5cb033e3116205dd502bee6f822717a8efe049d8ada2f02d1343a2c8b0cd3c93
+        checksum/config: 954486b77f49f95fc44392cf8ea7672033f74102bab63103d00a61ea7895c281
         linkerd.io/created-by: linkerd/helm dev-undefined
       labels:
         linkerd.io/extension: viz
diff --git a/web/app/js/components/TopRoutesTabs.jsx b/web/app/js/components/TopRoutesTabs.jsx
index 9bee35423..a66cf1b6e 100644
--- a/web/app/js/components/TopRoutesTabs.jsx
+++ b/web/app/js/components/TopRoutesTabs.jsx
@@ -6,6 +6,7 @@ import QueryToCliCmd from './QueryToCliCmd.jsx';
 import React from 'react';
 import Tab from '@material-ui/core/Tab';
 import Tabs from '@material-ui/core/Tabs';
+import TapEnabledWarning from './TapEnabledWarning.jsx';
 import TopModule from './TopModule.jsx';
 import TopRoutesModule from './TopRoutesModule.jsx';
 import { Trans } from '@lingui/macro';
@@ -51,7 +52,10 @@ class TopRoutesTabs extends React.Component {
           query={topQuery}
           startTap
           updateUnmeshedSources={updateUnmeshedSources}
-          maxRowsToDisplay={10} />
+          maxRowsToDisplay={10}
+          tapEnabledWarningComponent={<TapEnabledWarning
+            resource={topQuery.resource}
+            namespace={topQuery.namespace} />} />
         <QueryToCliCmd cmdName="top" query={topQuery} resource={topQuery.resource} />
       </React.Fragment>
     );