Repository: linkerd/linkerd2 (go)

Issue:
linkerd upgrade --ha fails with "failed to read CA: not PEM-encoded"
## Bug Report

### What is the issue?

`linkerd upgrade --ha` fails on `edge-20.12.3`

### How can it be reproduced?

Command succeeds prior to this commit, fails with it: https://github.com/linkerd/linkerd2/commit/35612ae26823a9dfec51facc927f1b68051cd255

```bash
# works
$ git checkout 377b38f0bfb83421a3b74434aea43188810a0d01
$ go run cli/main.go upgrade --ha
---
###
### Linkerd Namespace
...

# breaks on the next commit
$ git checkout 35612ae26823a9dfec51facc927f1b68051cd255
$ go run cli/main.go upgrade --ha
Error: failed to validate issuer credentials: failed to read CA: not PEM-encoded
Usage:
...
```

#### `linkerd check` output

```bash
$ linkerd check --context aks-dive-staging
kubernetes-api
--------------
√ can initialize the client
...
√ issuer cert is within its validity period
‼ issuer cert is valid for at least 60 days
    issuer certificate will expire on 2021-01-27T08:02:19Z
    see https://linkerd.io/checks/#l5d-identity-issuer-cert-not-expiring-soon for hints
√ issuer cert is issued by the trust anchor
...
control-plane-version
---------------------
‼ control plane is up-to-date
    is running version 20.12.1 but the latest edge version is 20.12.3
    see https://linkerd.io/checks/#l5d-version-control for hints
‼ control plane and cli versions match
    control plane running edge-20.12.1 but cli running edge-20.12.3
    see https://linkerd.io/checks/#l5d-version-control for hints
...
Status check results are √
```

### Environment

- Kubernetes Version: v1.18.2
- Cluster Environment: AKS
- Host OS: Linux
- Linkerd version: edge-20.12.3

### Additional context

We use cert-manager.


Pull request description:
Add missing label to linkerd-config-overrides secret
Fixes #5385 (second bug in there)

Added missing label `linkerd.io/control-plane-ns=linkerd` that all the
control plane resources must have, that is passed to `kubectl apply
--prune`


Patch:
diff --git a/charts/linkerd2/templates/destination-rbac.yaml b/charts/linkerd2/templates/destination-rbac.yaml
index 64e7c9a9a..744db7c4b 100644
--- a/charts/linkerd2/templates/destination-rbac.yaml
+++ b/charts/linkerd2/templates/destination-rbac.yaml
@@ -18,7 +18,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
diff --git a/cli/cmd/check.go b/cli/cmd/check.go
index 50aab13e1..2ea0c6476 100644
--- a/cli/cmd/check.go
+++ b/cli/cmd/check.go
@@ -221,7 +221,7 @@ func configureAndRunChecks(ctx context.Context, wout io.Writer, werr io.Writer,
 }
 
 func renderInstallManifest(ctx context.Context) (string, error) {
-	values, err := charts.NewValues(false)
+	values, err := charts.NewValues()
 	if err != nil {
 		return "", err
 	}
diff --git a/cli/cmd/inject.go b/cli/cmd/inject.go
index c1cfcfd19..603e857e6 100644
--- a/cli/cmd/inject.go
+++ b/cli/cmd/inject.go
@@ -48,7 +48,7 @@ func runInjectCmd(inputs []io.Reader, errWriter, outWriter io.Writer, transforme
 }
 
 func newCmdInject() *cobra.Command {
-	defaults, err := charts.NewValues(false)
+	defaults, err := charts.NewValues()
 	if err != nil {
 		fmt.Fprint(os.Stderr, err.Error())
 		os.Exit(1)
diff --git a/cli/cmd/install.go b/cli/cmd/install.go
index d0337867f..2399dfea8 100644
--- a/cli/cmd/install.go
+++ b/cli/cmd/install.go
@@ -211,7 +211,7 @@ control plane. It should be run after "linkerd install config".`,
 }
 
 func newCmdInstall() *cobra.Command {
-	values, err := l5dcharts.NewValues(false)
+	values, err := l5dcharts.NewValues()
 
 	allStageFlags, allStageFlagSet := makeAllStageFlags(values)
 	installOnlyFlags, installOnlyFlagSet := makeInstallFlags(values)
@@ -412,7 +412,7 @@ func render(w io.Writer, values *l5dcharts.Values, stage string) error {
 // resource is not part of the Helm chart and will not be present when installing
 // with Helm.
 func renderOverrides(values *l5dcharts.Values, namespace string) ([]byte, error) {
-	defaults, err := l5dcharts.NewValues(false)
+	defaults, err := l5dcharts.NewValues()
 	if err != nil {
 		return nil, err
 	}
@@ -433,6 +433,9 @@ func renderOverrides(values *l5dcharts.Values, namespace string) ([]byte, error)
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "linkerd-config-overrides",
 			Namespace: namespace,
+			Labels: map[string]string{
+				k8s.ControllerNSLabel: controlPlaneNamespace,
+			},
 		},
 		Data: map[string][]byte{
 			"linkerd-config-overrides": overridesBytes,
diff --git a/cli/cmd/options.go b/cli/cmd/options.go
index 05801b941..bad985a5d 100644
--- a/cli/cmd/options.go
+++ b/cli/cmd/options.go
@@ -54,12 +54,7 @@ func makeInstallUpgradeFlags(defaults *l5dcharts.Values) ([]flag.Flag, *pflag.Fl
 			func(values *l5dcharts.Values, value bool) error {
 				values.GetGlobal().HighAvailability = value
 				if value {
-					haValues, err := l5dcharts.NewValues(true)
-					if err != nil {
-						return err
-					}
-					*values, err = values.Merge(*haValues)
-					if err != nil {
+					if err := l5dcharts.MergeHAValues(values); err != nil {
 						return err
 					}
 				}
diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go
index fdca7e314..23f3cd2c6 100644
--- a/cli/cmd/upgrade.go
+++ b/cli/cmd/upgrade.go
@@ -114,7 +114,7 @@ install command. It should be run after "linkerd upgrade config".`,
 }
 
 func newCmdUpgrade() *cobra.Command {
-	values, err := l5dcharts.NewValues(false)
+	values, err := l5dcharts.NewValues()
 	if err != nil {
 		fmt.Fprint(os.Stderr, err.Error())
 		os.Exit(1)
@@ -290,7 +290,7 @@ func upgrade(ctx context.Context, k *k8s.KubernetesAPI, flags []flag.Flag, stage
 
 func loadStoredValues(ctx context.Context, k *k8s.KubernetesAPI) (*charts.Values, error) {
 	// Load the default values from the chart.
-	values, err := charts.NewValues(false)
+	values, err := charts.NewValues()
 	if err != nil {
 		return nil, err
 	}
@@ -361,7 +361,7 @@ func ensureIssuerCertWorksWithAllProxies(ctx context.Context, k *k8s.KubernetesA
 }
 
 func clearAddonOverrides(values *l5dcharts.Values) error {
-	defaults, err := l5dcharts.NewValues(false)
+	defaults, err := l5dcharts.NewValues()
 	if err != nil {
 		return err
 	}
diff --git a/cli/cmd/upgrade_legacy.go b/cli/cmd/upgrade_legacy.go
index 759cbdb1e..bb71e909c 100644
--- a/cli/cmd/upgrade_legacy.go
+++ b/cli/cmd/upgrade_legacy.go
@@ -32,7 +32,7 @@ func loadStoredValuesLegacy(ctx context.Context, k *k8s.KubernetesAPI) (*charts.
 	}
 	repairConfigs(configs)
 
-	values, err := charts.NewValues(false)
+	values, err := charts.NewValues()
 	if err != nil {
 		return nil, err
 	}
diff --git a/controller/api/destination/opaque_ports_adaptor.go b/controller/api/destination/opaque_ports_adaptor.go
new file mode 100644
index 000000000..0fcf9da0f
--- /dev/null
+++ b/controller/api/destination/opaque_ports_adaptor.go
@@ -0,0 +1,133 @@
+package destination
+
+import (
+	"fmt"
+	"strconv"
+
+	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
+	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
+	"github.com/linkerd/linkerd2/controller/k8s"
+	pkgk8s "github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/linkerd/linkerd2/pkg/util"
+	logging "github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
+)
+
+// opaquePortsAdaptor implements EndpointUpdateListener so that it can watch
+// endpoints for services. When endpoints change, it can check for the opaque
+// ports annotation on the pods and update the list of opaque ports.
+//
+// opaquePortsAdaptor also implements ProfileUpdateListener so that it can
+// receive updates from trafficSplitAdaptor and merge the service profile by
+// adding the list of opaque ports.
+//
+// When either of these implemented interfaces has an update,
+// opaquePortsAdaptor publishes the new service profile to the
+// profileTranslator.
+type opaquePortsAdaptor struct {
+	listener    watcher.ProfileUpdateListener
+	k8sAPI      *k8s.API
+	log         *logging.Entry
+	profile     *sp.ServiceProfile
+	opaquePorts map[uint32]struct{}
+}
+
+func newOpaquePortsAdaptor(listener watcher.ProfileUpdateListener, k8sAPI *k8s.API, log *logging.Entry) *opaquePortsAdaptor {
+	return &opaquePortsAdaptor{
+		listener:    listener,
+		k8sAPI:      k8sAPI,
+		log:         log,
+		opaquePorts: make(map[uint32]struct{}),
+	}
+}
+
+func (opa *opaquePortsAdaptor) Add(set watcher.AddressSet) {
+	ports := opa.getOpaquePorts(set)
+	for port := range ports {
+		opa.opaquePorts[port] = struct{}{}
+	}
+	opa.publish()
+}
+
+func (opa *opaquePortsAdaptor) Remove(set watcher.AddressSet) {
+	ports := opa.getOpaquePorts(set)
+	for port := range ports {
+		delete(opa.opaquePorts, port)
+	}
+	opa.publish()
+}
+
+func (opa *opaquePortsAdaptor) NoEndpoints(exists bool) {
+	opa.opaquePorts = make(map[uint32]struct{})
+	opa.publish()
+}
+
+func (opa *opaquePortsAdaptor) Update(profile *sp.ServiceProfile) {
+	opa.profile = profile
+	opa.publish()
+}
+
+func (opa *opaquePortsAdaptor) getOpaquePorts(set watcher.AddressSet) map[uint32]struct{} {
+	ports := make(map[uint32]struct{})
+	for _, address := range set.Addresses {
+		pod := address.Pod
+		if pod != nil {
+			override, err := getOpaquePortsAnnotations(opa.k8sAPI, pod)
+			if err != nil {
+				opa.log.Errorf("Failed to get opaque ports annotation for pod %s: %s", pod, err)
+			}
+			for port := range override {
+				ports[port] = struct{}{}
+			}
+		}
+	}
+	return ports
+}
+
+func (opa *opaquePortsAdaptor) publish() {
+	merged := sp.ServiceProfile{}
+	if opa.profile != nil {
+		merged = *opa.profile
+	}
+	merged.Spec.OpaquePorts = opa.opaquePorts
+	fmt.Printf("publishing SP: %v", merged)
+	opa.listener.Update(&merged)
+}
+
+func getOpaquePortsAnnotations(k8sAPI *k8s.API, pod *corev1.Pod) (map[uint32]struct{}, error) {
+	opaquePorts := make(map[uint32]struct{})
+	obj, err := k8sAPI.GetObjects("", pkgk8s.Namespace, pod.Namespace, labels.Everything())
+	if err != nil {
+		return nil, err
+	}
+	if len(obj) > 1 {
+		return nil, fmt.Errorf("Namespace conflict: %v, %v", obj[0], obj[1])
+	}
+	if len(obj) != 1 {
+		return nil, fmt.Errorf("Namespace not found: %v", pod.Namespace)
+	}
+	ns, ok := obj[0].(*corev1.Namespace)
+	if !ok {
+		// This is very unlikely due to how `GetObjects` works
+		return nil, fmt.Errorf("Object with name %s was not a namespace", pod.Namespace)
+	}
+	override := ns.Annotations[pkgk8s.ProxyOpaquePortsAnnotation]
+
+	// Pod annotations override namespace annotations
+	if podOverride, ok := pod.Annotations[pkgk8s.ProxyOpaquePortsAnnotation]; ok {
+		override = podOverride
+	}
+
+	if override != "" {
+		for _, portStr := range util.ParseOpaquePorts(override, pod.Spec.Containers) {
+			port, err := strconv.ParseUint(portStr, 10, 32)
+			if err != nil {
+				return nil, err
+			}
+			opaquePorts[uint32(port)] = struct{}{}
+		}
+	}
+
+	return opaquePorts, nil
+}
diff --git a/controller/api/destination/profile_translator.go b/controller/api/destination/profile_translator.go
index 97b910794..3fea68825 100644
--- a/controller/api/destination/profile_translator.go
+++ b/controller/api/destination/profile_translator.go
@@ -2,12 +2,10 @@ package destination
 
 import (
 	"errors"
-	"fmt"
 	"time"
 
 	"github.com/golang/protobuf/ptypes/duration"
 	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
-	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
 	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	"github.com/linkerd/linkerd2/pkg/profiles"
 	"github.com/linkerd/linkerd2/pkg/util"
@@ -21,18 +19,16 @@ type profileTranslator struct {
 	stream             pb.Destination_GetProfileServer
 	log                *logging.Entry
 	fullyQualifiedName string
+	port               uint32
 	endpoint           *pb.WeightedAddr
 }
 
-func newProfileTranslator(stream pb.Destination_GetProfileServer, log *logging.Entry, id *watcher.ServiceID, clusterDomain string, endpoint *pb.WeightedAddr) *profileTranslator {
-	var fullyQualifiedName string
-	if id != nil {
-		fullyQualifiedName = fmt.Sprintf("%s.%s.svc.%s", id.Name, id.Namespace, clusterDomain)
-	}
+func newProfileTranslator(stream pb.Destination_GetProfileServer, log *logging.Entry, fqn string, port uint32, endpoint *pb.WeightedAddr) *profileTranslator {
 	return &profileTranslator{
 		stream:             stream,
 		log:                log.WithField("component", "profile-translator"),
-		fullyQualifiedName: fullyQualifiedName,
+		fullyQualifiedName: fqn,
+		port:               port,
 		endpoint:           endpoint,
 	}
 }
@@ -101,11 +97,17 @@ func (pt *profileTranslator) toServiceProfile(profile *sp.ServiceProfile) (*pb.D
 		}
 		budget.Ttl = toDuration(ttl)
 	}
+	var opaqueProtocol bool
+	if profile.Spec.OpaquePorts != nil {
+		_, opaqueProtocol = profile.Spec.OpaquePorts[pt.port]
+	}
 	return &pb.DestinationProfile{
 		Routes:             routes,
 		RetryBudget:        budget,
 		DstOverrides:       toDstOverrides(profile.Spec.DstOverrides),
 		FullyQualifiedName: pt.fullyQualifiedName,
+		Endpoint:           pt.endpoint,
+		OpaqueProtocol:     opaqueProtocol,
 	}, nil
 }
 
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index 12d08b0c6..ba5b0351d 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -9,6 +9,7 @@ import (
 
 	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	"github.com/linkerd/linkerd2/controller/api/destination/watcher"
+	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	"github.com/linkerd/linkerd2/pkg/prometheus"
 	logging "github.com/sirupsen/logrus"
@@ -165,18 +166,21 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 	}
 	log.Debugf("GetProfile(%+v)", dest)
 
+	path := dest.GetPath()
 	// The host must be fully-qualified or be an IP address.
-	host, port, err := getHostAndPort(dest.GetPath())
+	host, port, err := getHostAndPort(path)
 	if err != nil {
-		log.Debugf("Invalid authority %s", dest.GetPath())
+		log.Debugf("Invalid authority %s", path)
 		return status.Errorf(codes.InvalidArgument, "invalid authority: %s", err)
 	}
 
 	// The stream will subscribe to profile updates for `service`.
 	var service watcher.ServiceID
-	// If `host` is an IP, path must be constructed from the namespace and
+	// `instanceID` is used when subscribing to endpoints of `service`.
+	var instanceID string
+	// If `host` is an IP, `fqn` must be constructed from the namespace and
 	// name of the service that the IP maps to.
-	var path string
+	var fqn string
 
 	if ip := net.ParseIP(host); ip != nil {
 		// Get the service that the IP currently maps to.
@@ -186,14 +190,16 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 		}
 		if svcID != nil {
 			service = *svcID
-			path = fmt.Sprintf("%s.%s.svc.%s", service.Name, service.Namespace, s.clusterDomain)
+			fqn = fmt.Sprintf("%s.%s.svc.%s", service.Name, service.Namespace, s.clusterDomain)
 		} else {
 			// If the IP does not map to a service, check if it maps to a pod
 			pod, err := s.ips.GetPod(ip.String())
 			if err != nil {
 				return err
 			}
+
 			var endpoint *pb.WeightedAddr
+			opaquePorts := make(map[uint32]struct{})
 			if pod != nil {
 				// If the IP maps to a pod, we create a single endpoint and
 				// return it in the DestinationProfile response
@@ -203,9 +209,15 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 					Name:      pod.Name,
 				}
 				endpoint, err = toWeightedAddr(podSet.Addresses[podID], s.enableH2Upgrade, s.identityTrustDomain, s.controllerNS)
+				if err != nil {
+					return err
+				}
+
 				// `Get` doesn't include the namespace in the per-endpoint
 				// metadata, so it needs to be special-cased.
 				endpoint.MetricLabels["namespace"] = pod.Namespace
+
+				opaquePorts, err = getOpaquePortsAnnotations(s.k8sAPI, pod)
 				if err != nil {
 					return err
 				}
@@ -214,8 +226,20 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 			// When the IP does not map to a service, the default profile is
 			// sent without subscribing for future updates. If the IP mapped
 			// to a pod, then the endpoint will be set in the response.
-			translator := newProfileTranslator(stream, log, nil, "", endpoint)
-			translator.Update(nil)
+			translator := newProfileTranslator(stream, log, "", port, endpoint)
+
+			// If there are opaque ports then update the profile translator
+			// with a service profile that has those values
+			//
+			// TODO: Remove endpoint from profileTranslator and set it here
+			// similar to opaque ports
+			if len(opaquePorts) != 0 {
+				sp := sp.ServiceProfile{}
+				sp.Spec.OpaquePorts = opaquePorts
+				translator.Update(&sp)
+			} else {
+				translator.Update(nil)
+			}
 
 			select {
 			case <-s.shutdown:
@@ -226,22 +250,34 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 			return nil
 		}
 	} else {
-		service, _, err = parseK8sServiceName(host, s.clusterDomain)
+		service, instanceID, err = parseK8sServiceName(host, s.clusterDomain)
 		if err != nil {
-			log.Debugf("Invalid service %s", dest.GetPath())
+			log.Debugf("Invalid service %s", path)
 			return status.Errorf(codes.InvalidArgument, "invalid service: %s", err)
 		}
-		path = dest.GetPath()
+		fqn = host
 	}
 
 	// We build up the pipeline of profile updaters backwards, starting from
 	// the translator which takes profile updates, translates them to protobuf
 	// and pushes them onto the gRPC stream.
-	translator := newProfileTranslator(stream, log, &service, s.clusterDomain, nil)
+	translator := newProfileTranslator(stream, log, fqn, port, nil)
+
+	// The opaque ports adaptor merges traffic split updates with opaque ports
+	// updates and publishes the result to the translator
+	opAdaptor := newOpaquePortsAdaptor(translator, s.k8sAPI, log)
+
+	// Subscribe the adaptor to endpoint updates.
+	// TODO: use a hostname?
+	err = s.endpoints.Subscribe(service, port, instanceID, opAdaptor)
+	if err != nil {
+		log.Warnf("Failed to subscribe to endpoint updates for %s: %s", path, err)
+	}
+	defer s.endpoints.Unsubscribe(service, port, instanceID, opAdaptor)
 
-	// The adaptor merges profile updates with traffic split updates and
-	// publishes the result to the translator.
-	tsAdaptor := newTrafficSplitAdaptor(translator, service, port, s.clusterDomain)
+	// The traffic split adaptor merges profile updates with traffic split
+	// updates and publishes the result to the opaque port adaptor.
+	tsAdaptor := newTrafficSplitAdaptor(opAdaptor, service, port, s.clusterDomain)
 
 	// Subscribe the adaptor to traffic split updates.
 	err = s.trafficSplits.Subscribe(service, tsAdaptor)
@@ -263,7 +299,7 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 	if dest.GetContextToken() != "" {
 		ctxToken := s.parseContextToken(dest.GetContextToken())
 
-		profile, err := profileID(path, ctxToken, s.clusterDomain)
+		profile, err := profileID(fqn, ctxToken, s.clusterDomain)
 		if err != nil {
 			log.Debugf("Invalid service %s", path)
 			return status.Errorf(codes.InvalidArgument, "invalid profile ID: %s", err)
@@ -277,7 +313,7 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 		defer s.profiles.Unsubscribe(profile, primary)
 	}
 
-	profile, err := profileID(path, contextToken{}, s.clusterDomain)
+	profile, err := profileID(fqn, contextToken{}, s.clusterDomain)
 	if err != nil {
 		log.Debugf("Invalid service %s", path)
 		return status.Errorf(codes.InvalidArgument, "invalid profile ID: %s", err)
diff --git a/controller/cmd/destination/main.go b/controller/cmd/destination/main.go
index 7d79fb21d..b34c22f01 100644
--- a/controller/cmd/destination/main.go
+++ b/controller/cmd/destination/main.go
@@ -84,14 +84,14 @@ func Main(args []string) {
 			ctx,
 			*kubeConfigPath,
 			true,
-			k8s.Endpoint, k8s.ES, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job,
+			k8s.Endpoint, k8s.ES, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS,
 		)
 	} else {
 		k8sAPI, err = k8s.InitializeAPI(
 			ctx,
 			*kubeConfigPath,
 			true,
-			k8s.Endpoint, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job,
+			k8s.Endpoint, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS,
 		)
 	}
 	if err != nil {
diff --git a/controller/gen/apis/serviceprofile/v1alpha2/types.go b/controller/gen/apis/serviceprofile/v1alpha2/types.go
index d60820ab7..396a0a785 100644
--- a/controller/gen/apis/serviceprofile/v1alpha2/types.go
+++ b/controller/gen/apis/serviceprofile/v1alpha2/types.go
@@ -28,9 +28,10 @@ type ServiceProfile struct {
 
 // ServiceProfileSpec specifies a ServiceProfile resource.
 type ServiceProfileSpec struct {
-	Routes       []*RouteSpec   `json:"routes"`
-	RetryBudget  *RetryBudget   `json:"retryBudget,omitempty"`
-	DstOverrides []*WeightedDst `json:"dstOverrides,omitempty"`
+	Routes       []*RouteSpec        `json:"routes"`
+	RetryBudget  *RetryBudget        `json:"retryBudget,omitempty"`
+	DstOverrides []*WeightedDst      `json:"dstOverrides,omitempty"`
+	OpaquePorts  map[uint32]struct{} `json:"opaquePorts,omitempty"`
 }
 
 // RouteSpec specifies a Route resource.
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index 2f9a76b3e..2b9270fc1 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -250,9 +250,8 @@ type (
 )
 
 // NewValues returns a new instance of the Values type.
-func NewValues(ha bool) (*Values, error) {
-	chartDir := fmt.Sprintf("%s/", helmDefaultChartDir)
-	v, err := readDefaults(chartDir, ha)
+func NewValues() (*Values, error) {
+	v, err := readDefaults(false)
 	if err != nil {
 		return nil, err
 	}
@@ -269,38 +268,34 @@ func NewValues(ha bool) (*Values, error) {
 	return v, nil
 }
 
-// readDefaults read all the default variables from the values.yaml file.
-// chartDir is the root directory of the Helm chart where values.yaml is.
-func readDefaults(chartDir string, ha bool) (*Values, error) {
-	valuesFiles := []*loader.BufferedFile{
-		{Name: chartutil.ValuesfileName},
+// MergeHAValues retrieves the default HA values and merges them into the received values
+func MergeHAValues(values *Values) error {
+	haValues, err := readDefaults(true)
+	if err != nil {
+		return err
 	}
+	*values, err = values.Merge(*haValues)
+	return err
+}
 
+// readDefaults read all the default variables from the values.yaml file.
+func readDefaults(ha bool) (*Values, error) {
+	var valuesFile *loader.BufferedFile
 	if ha {
-		valuesFiles = append(valuesFiles, &loader.BufferedFile{
-			Name: helmDefaultHAValuesFile,
-		})
+		valuesFile = &loader.BufferedFile{Name: helmDefaultHAValuesFile}
+	} else {
+		valuesFile = &loader.BufferedFile{Name: chartutil.ValuesfileName}
 	}
 
-	if err := charts.FilesReader(static.Templates, chartDir, valuesFiles); err != nil {
+	chartDir := fmt.Sprintf("%s/", helmDefaultChartDir)
+	if err := charts.ReadFile(static.Templates, chartDir, valuesFile); err != nil {
 		return nil, err
 	}
 
-	values := Values{}
-	for _, valuesFile := range valuesFiles {
-		var v Values
-		if err := yaml.Unmarshal(charts.InsertVersion(valuesFile.Data), &v); err != nil {
-			return nil, err
-		}
-
-		var err error
-		values, err = values.Merge(v)
-		if err != nil {
-			return nil, err
-		}
-	}
+	var values Values
+	err := yaml.Unmarshal(charts.InsertVersion(valuesFile.Data), &values)
 
-	return &values, nil
+	return &values, err
 }
 
 // Merge merges the non-empty properties of src into v.
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index b8f42610f..59efbf9d9 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -13,13 +13,12 @@ import (
 	"time"
 
 	jsonfilter "github.com/clarketm/json"
-	"github.com/linkerd/linkerd2-proxy-init/ports"
-	"github.com/linkerd/linkerd2/controller/gen/config"
 	"github.com/linkerd/linkerd2/pkg/charts"
 	"github.com/linkerd/linkerd2/pkg/charts/linkerd2"
 	l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
 	"github.com/linkerd/linkerd2/pkg/charts/static"
 	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/linkerd/linkerd2/pkg/util"
 	log "github.com/sirupsen/logrus"
 	"helm.sh/helm/v3/pkg/chart/loader"
 	"helm.sh/helm/v3/pkg/chartutil"
@@ -827,39 +826,8 @@ func (conf *ResourceConfig) applyAnnotationOverrides(values *l5dcharts.Values) {
 	}
 
 	if override, ok := annotations[k8s.ProxyOpaquePortsAnnotation]; ok {
-		var portRanges []*config.PortRange
-		split := strings.Split(strings.TrimSuffix(override, ","), ",")
-		portRanges = ToPortRanges(split)
-
-		var str string
-		for _, portRange := range portRanges {
-			pr := portRange.GetPortRange()
-
-			// It is valid for the format of a port name to be the same as a
-			// port range (e.g. `123-456` is a valid name, but also is a valid
-			// range). All port names must be checked before making it a list.
-			named := false
-			for _, c := range conf.pod.spec.Containers {
-				for _, p := range c.Ports {
-					if p.Name == pr {
-						named = true
-						str += strconv.Itoa(int(p.ContainerPort)) + ","
-					}
-				}
-			}
-
-			if !named {
-				pr, err := ports.ParsePortRange(pr)
-				if err != nil {
-					log.Warnf("Invalid port range [%v]: %s", pr, err)
-					continue
-				}
-				for i := pr.LowerBound; i <= pr.UpperBound; i++ {
-					str += strconv.Itoa(i) + ","
-				}
-			}
-		}
-		values.GetGlobal().Proxy.OpaquePorts = strings.TrimSuffix(str, ",")
+		opaquePortsStrs := util.ParseOpaquePorts(override, conf.pod.spec.Containers)
+		values.GetGlobal().Proxy.OpaquePorts = strings.Join(opaquePortsStrs, ",")
 	}
 
 	if override, ok := annotations[k8s.DebugImageAnnotation]; ok {
@@ -936,15 +904,6 @@ func getFilteredJSON(conf runtime.Object) ([]byte, error) {
 	return jsonfilter.Marshal(&conf)
 }
 
-// ToPortRanges converts a slice of strings into a slice of PortRanges.
-func ToPortRanges(portRanges []string) []*config.PortRange {
-	ports := make([]*config.PortRange, len(portRanges))
-	for i, p := range portRanges {
-		ports[i] = &config.PortRange{PortRange: p}
-	}
-	return ports
-}
-
 // ToWholeCPUCores coerces a k8s resource value to a whole integer value, rounding up.
 func ToWholeCPUCores(q k8sResource.Quantity) (int64, error) {
 	q.RoundUp(0)
diff --git a/pkg/util/parsing.go b/pkg/util/parsing.go
new file mode 100644
index 000000000..b721abde6
--- /dev/null
+++ b/pkg/util/parsing.go
@@ -0,0 +1,59 @@
+package util
+
+import (
+	"strconv"
+	"strings"
+
+	"github.com/linkerd/linkerd2-proxy-init/ports"
+	"github.com/linkerd/linkerd2/controller/gen/config"
+	"github.com/prometheus/common/log"
+	corev1 "k8s.io/api/core/v1"
+)
+
+// ParseOpaquePorts 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.
+func ParseOpaquePorts(override string, containers []corev1.Container) []string {
+	var portRanges []*config.PortRange
+	split := strings.Split(strings.TrimSuffix(override, ","), ",")
+	portRanges = ToPortRanges(split)
+
+	var values []string
+	for _, portRange := range portRanges {
+		pr := portRange.GetPortRange()
+
+		// It is valid for the format of a port name to be the same as a
+		// port range (e.g. `123-456` is a valid name, but also is a valid
+		// range). All port names must be checked before making it a list.
+		named := false
+		for _, c := range containers {
+			for _, p := range c.Ports {
+				if p.Name == pr {
+					named = true
+					values = append(values, strconv.Itoa(int(p.ContainerPort)))
+				}
+			}
+		}
+
+		if !named {
+			pr, err := ports.ParsePortRange(pr)
+			if err != nil {
+				log.Warnf("Invalid port range [%v]: %s", pr, err)
+				continue
+			}
+			for i := pr.LowerBound; i <= pr.UpperBound; i++ {
+				values = append(values, strconv.Itoa(i))
+			}
+		}
+	}
+	return values
+}
+
+// ToPortRanges converts a slice of strings into a slice of PortRanges.
+func ToPortRanges(portRanges []string) []*config.PortRange {
+	ports := make([]*config.PortRange, len(portRanges))
+	for i, p := range portRanges {
+		ports[i] = &config.PortRange{PortRange: p}
+	}
+	return ports
+}


Test patch:
diff --git a/cli/cmd/inject_test.go b/cli/cmd/inject_test.go
index 0f328b3e6..9668b143a 100644
--- a/cli/cmd/inject_test.go
+++ b/cli/cmd/inject_test.go
@@ -575,7 +575,7 @@ func TestWalk(t *testing.T) {
 }
 
 func TestProxyConfigurationAnnotations(t *testing.T) {
-	baseValues, err := linkerd2.NewValues(false)
+	baseValues, err := linkerd2.NewValues()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -627,7 +627,7 @@ func TestProxyConfigurationAnnotations(t *testing.T) {
 }
 
 func TestProxyImageAnnotations(t *testing.T) {
-	baseValues, err := linkerd2.NewValues(false)
+	baseValues, err := linkerd2.NewValues()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -653,7 +653,7 @@ func TestProxyImageAnnotations(t *testing.T) {
 }
 
 func TestProxyInitImageAnnotations(t *testing.T) {
-	baseValues, err := linkerd2.NewValues(false)
+	baseValues, err := linkerd2.NewValues()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -677,7 +677,7 @@ func TestProxyInitImageAnnotations(t *testing.T) {
 }
 
 func TestNoAnnotations(t *testing.T) {
-	baseValues, err := linkerd2.NewValues(false)
+	baseValues, err := linkerd2.NewValues()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/cli/cmd/install_helm_test.go b/cli/cmd/install_helm_test.go
index be57750db..ccf21ad53 100644
--- a/cli/cmd/install_helm_test.go
+++ b/cli/cmd/install_helm_test.go
@@ -332,10 +332,15 @@ func chartPartials(t *testing.T, paths []string) *chart.Chart {
 }
 
 func readTestValues(ha bool, ignoreOutboundPorts string, ignoreInboundPorts string) (*l5dcharts.Values, error) {
-	values, err := l5dcharts.NewValues(ha)
+	values, err := l5dcharts.NewValues()
 	if err != nil {
 		return nil, err
 	}
+	if ha {
+		if err = l5dcharts.MergeHAValues(values); err != nil {
+			return nil, err
+		}
+	}
 	values.GetGlobal().ProxyInit.IgnoreOutboundPorts = ignoreOutboundPorts
 	values.GetGlobal().ProxyInit.IgnoreInboundPorts = ignoreInboundPorts
 
diff --git a/cli/cmd/install_test.go b/cli/cmd/install_test.go
index 08cbf8c5e..10fa5dbd9 100644
--- a/cli/cmd/install_test.go
+++ b/cli/cmd/install_test.go
@@ -307,10 +307,15 @@ func testInstallOptionsHA(ha bool) (*charts.Values, error) {
 }
 
 func testInstallOptionsNoCerts(ha bool) (*charts.Values, error) {
-	values, err := charts.NewValues(ha)
+	values, err := charts.NewValues()
 	if err != nil {
 		return nil, err
 	}
+	if ha {
+		if err = charts.MergeHAValues(values); err != nil {
+			return nil, err
+		}
+	}
 
 	values.GetGlobal().Proxy.Image.Version = installProxyVersion
 	values.DebugContainer.Image.Version = installDebugVersion
@@ -321,7 +326,7 @@ func testInstallOptionsNoCerts(ha bool) (*charts.Values, error) {
 }
 
 func testInstallValues() (*charts.Values, error) {
-	values, err := charts.NewValues(false)
+	values, err := charts.NewValues()
 	if err != nil {
 		return nil, err
 	}
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 7470327e9..f58e027b5 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3482,5 +3482,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 9d010353c..628614491 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3477,5 +3477,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 3c2903f63..54786f9cf 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3477,5 +3477,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
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 9e0606428..2dbc8dfb4 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3477,5 +3477,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_grafana_existing.golden b/cli/cmd/testdata/install_grafana_existing.golden
index 8d1e2db87..b8bf4a4f5 100644
--- a/cli/cmd/testdata/install_grafana_existing.golden
+++ b/cli/cmd/testdata/install_grafana_existing.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3155,5 +3155,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 102f86219..f736189e7 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3780,5 +3780,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 3b142d812..4867e54c9 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3780,5 +3780,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index f9b593d0b..d19e2402c 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3388,5 +3388,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index 06ab727c0..912a21820 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -141,7 +141,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
diff --git a/cli/cmd/testdata/install_helm_output_addons.golden b/cli/cmd/testdata/install_helm_output_addons.golden
index 06ab727c0..912a21820 100644
--- a/cli/cmd/testdata/install_helm_output_addons.golden
+++ b/cli/cmd/testdata/install_helm_output_addons.golden
@@ -141,7 +141,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index fb2fd3687..2d266520b 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -141,7 +141,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index d78b56c9a..32d289f5f 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -141,7 +141,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
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 f1fa14f57..cad5e76e5 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -141,7 +141,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index d67d5f6b8..3c9bbdaae 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3132,5 +3132,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index b381a9334..98abe0258 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3496,5 +3496,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_prometheus_overwrite.golden b/cli/cmd/testdata/install_prometheus_overwrite.golden
index 6e76aa678..edcacaaff 100644
--- a/cli/cmd/testdata/install_prometheus_overwrite.golden
+++ b/cli/cmd/testdata/install_prometheus_overwrite.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3593,5 +3593,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index e4d450875..3682c4d0c 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3477,5 +3477,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_restricted_dashboard.golden b/cli/cmd/testdata/install_restricted_dashboard.golden
index 1fc7e48da..02e634ab3 100644
--- a/cli/cmd/testdata/install_restricted_dashboard.golden
+++ b/cli/cmd/testdata/install_restricted_dashboard.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
@@ -3409,5 +3409,7 @@ data:
 kind: Secret
 metadata:
   creationTimestamp: null
+  labels:
+    linkerd.io/control-plane-ns: linkerd
   name: linkerd-config-overrides
   namespace: linkerd
diff --git a/cli/cmd/testdata/install_tracing_overwrite.golden b/cli/cmd/testdata/install_tracing_overwrite.golden
index 6ef70965a..ecf426865 100644
--- a/cli/cmd/testdata/install_tracing_overwrite.golden
+++ b/cli/cmd/testdata/install_tracing_overwrite.golden
@@ -133,7 +133,7 @@ rules:
   resources: ["jobs"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
-  resources: ["pods", "endpoints", "services", "nodes"]
+  resources: ["pods", "endpoints", "services", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
diff --git a/cli/cmd/uninject_test.go b/cli/cmd/uninject_test.go
index bb957894f..ae1312dfc 100644
--- a/cli/cmd/uninject_test.go
+++ b/cli/cmd/uninject_test.go
@@ -93,7 +93,7 @@ func TestUninjectYAML(t *testing.T) {
 		},
 	}
 
-	values, err := charts.NewValues(false)
+	values, err := charts.NewValues()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/cli/cmd/upgrade_test.go b/cli/cmd/upgrade_test.go
index fc64476cf..6640a53c3 100644
--- a/cli/cmd/upgrade_test.go
+++ b/cli/cmd/upgrade_test.go
@@ -544,7 +544,7 @@ func TestUpgradeOverwriteRemoveAddonKeys(t *testing.T) {
 /* Helpers */
 
 func testUpgradeOptions() ([]flag.Flag, *pflag.FlagSet, error) {
-	defaults, err := charts.NewValues(false)
+	defaults, err := charts.NewValues()
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/controller/api/destination/server_test.go b/controller/api/destination/server_test.go
index b66c36a88..031ce6b54 100644
--- a/controller/api/destination/server_test.go
+++ b/controller/api/destination/server_test.go
@@ -18,6 +18,7 @@ const clusterIP = "172.17.12.0"
 const podIP1 = "172.17.0.12"
 const podIP2 = "172.17.0.13"
 const port uint32 = 8989
+const opaquePort uint32 = 4242
 
 type mockDestinationGetServer struct {
 	util.MockServerStream
@@ -42,6 +43,11 @@ func (m *mockDestinationGetProfileServer) Send(profile *pb.DestinationProfile) e
 func makeServer(t *testing.T) *server {
 	k8sAPI, err := k8s.NewFakeAPI(`
 apiVersion: v1
+kind: Namespace
+metadata:
+  name: ns`,
+		`
+apiVersion: v1
 kind: Service
 metadata:
   name: name1
@@ -72,6 +78,8 @@ kind: Pod
 metadata:
   labels:
     linkerd.io/control-plane-ns: linkerd
+  annotations:
+    config.linkerd.io/opaque-ports: "4242"
   name: name1-1
   namespace: ns
 status:
@@ -260,6 +268,9 @@ func TestGetProfiles(t *testing.T) {
 		}
 
 		lastUpdate := stream.updates[len(stream.updates)-1]
+		if lastUpdate.OpaqueProtocol {
+			t.Fatalf("Expected port %d to not be an opaque protocol, but it was", port)
+		}
 		routes := lastUpdate.GetRoutes()
 		if len(routes) != 1 {
 			t.Fatalf("Expected 1 route but got %d: %v", len(routes), routes)
@@ -411,11 +422,14 @@ func TestGetProfiles(t *testing.T) {
 			t.Fatalf("Expected 1 to 3 updates but got %d: %v", len(stream.updates), stream.updates)
 		}
 
-		first := stream.updates[0]
-		if first.FullyQualifiedName != fullyQualifiedName {
-			t.Fatalf("Expected fully qualified name '%s', but got '%s'", fullyQualifiedName, first.FullyQualifiedName)
+		last := stream.updates[len(stream.updates)-1]
+		if last.FullyQualifiedName != fullyQualifiedName {
+			t.Fatalf("Expected fully qualified name '%s', but got '%s'", fullyQualifiedName, last.FullyQualifiedName)
 		}
-		routes := first.GetRoutes()
+		if last.OpaqueProtocol {
+			t.Fatalf("Expected port %d to not be an opaque protocol, but it was", port)
+		}
+		routes := last.GetRoutes()
 		if len(routes) != 1 {
 			t.Fatalf("Expected 1 route but got %d: %v", len(routes), routes)
 		}
@@ -452,6 +466,9 @@ func TestGetProfiles(t *testing.T) {
 		if first.Endpoint == nil {
 			t.Fatalf("Expected response to have endpoint field")
 		}
+		if first.OpaqueProtocol {
+			t.Fatalf("Expected port %d to not be an opaque protocol, but it was", port)
+		}
 		_, exists := first.Endpoint.MetricLabels["namespace"]
 		if !exists {
 			t.Fatalf("Expected 'namespace' metric label to exist but it did not")
@@ -520,6 +537,89 @@ func TestGetProfiles(t *testing.T) {
 			t.Fatalf("Expected no protocol hint but found one")
 		}
 	})
+
+	t.Run("Return opaque protocol profile when using cluster IP and opaque protocol port", func(t *testing.T) {
+		server := makeServer(t)
+		stream := &bufferingGetProfileStream{
+			updates:          []*pb.DestinationProfile{},
+			MockServerStream: util.NewMockServerStream(),
+		}
+		stream.Cancel()
+		err := server.GetProfile(&pb.GetDestination{
+			Scheme: "k8s",
+			Path:   fmt.Sprintf("%s:%d", clusterIP, opaquePort),
+		}, stream)
+		if err != nil {
+			t.Fatalf("Got error: %s", err)
+		}
+
+		// An explanation for why we expect 1 to 3 updates is in test cases
+		// above
+		if len(stream.updates) == 0 || len(stream.updates) > 3 {
+			t.Fatalf("Expected 1 to 3 updates but got %d: %v", len(stream.updates), stream.updates)
+		}
+
+		last := stream.updates[len(stream.updates)-1]
+		if last.FullyQualifiedName != fullyQualifiedName {
+			t.Fatalf("Expected fully qualified name '%s', but got '%s'", fullyQualifiedName, last.FullyQualifiedName)
+		}
+		if !last.OpaqueProtocol {
+			t.Fatalf("Expected port %d to be an opaque protocol, but it was not", opaquePort)
+		}
+		routes := last.GetRoutes()
+		if len(routes) != 1 {
+			t.Fatalf("Expected 1 route but got %d: %v", len(routes), routes)
+		}
+	})
+
+	t.Run("Return opaque protocol profile with endpoint when using pod IP and opaque protocol port", func(t *testing.T) {
+		server := makeServer(t)
+		stream := &bufferingGetProfileStream{
+			updates:          []*pb.DestinationProfile{},
+			MockServerStream: util.NewMockServerStream(),
+		}
+		stream.Cancel()
+
+		epAddr, err := toAddress(podIP1, opaquePort)
+		if err != nil {
+			t.Fatalf("Got error: %s", err)
+		}
+
+		err = server.GetProfile(&pb.GetDestination{
+			Scheme: "k8s",
+			Path:   fmt.Sprintf("%s:%d", podIP1, opaquePort),
+		}, stream)
+		if err != nil {
+			t.Fatalf("Got error: %s", err)
+		}
+
+		// An explanation for why we expect 1 to 3 updates is in test cases
+		// above
+		if len(stream.updates) == 0 || len(stream.updates) > 3 {
+			t.Fatalf("Expected 1 to 3 updates but got %d: %v", len(stream.updates), stream.updates)
+		}
+
+		first := stream.updates[0]
+		if first.Endpoint == nil {
+			t.Fatalf("Expected response to have endpoint field")
+		}
+		if !first.OpaqueProtocol {
+			t.Fatalf("Expected port %d to be an opaque protocol, but it was not", opaquePort)
+		}
+		_, exists := first.Endpoint.MetricLabels["namespace"]
+		if !exists {
+			t.Fatalf("Expected 'namespace' metric label to exist but it did not")
+		}
+		if first.Endpoint.ProtocolHint == nil {
+			t.Fatalf("Expected protocol hint but found none")
+		}
+		if !first.OpaqueProtocol {
+			t.Fatalf("Expected protocol to be opaque but it was not")
+		}
+		if first.Endpoint.Addr.String() != epAddr.String() {
+			t.Fatalf("Expected endpoint IP port to be %d, but it was %d", epAddr.Port, first.Endpoint.Addr.Port)
+		}
+	})
 }
 
 func TestTokenStructure(t *testing.T) {
diff --git a/controller/proxy-injector/webhook_test.go b/controller/proxy-injector/webhook_test.go
index 20e803632..e6a9e3298 100644
--- a/controller/proxy-injector/webhook_test.go
+++ b/controller/proxy-injector/webhook_test.go
@@ -21,7 +21,7 @@ import (
 type unmarshalledPatch []map[string]interface{}
 
 var (
-	values, _ = linkerd2.NewValues(false)
+	values, _ = linkerd2.NewValues()
 )
 
 func confNsEnabled() *inject.ResourceConfig {
diff --git a/pkg/charts/linkerd2/values_test.go b/pkg/charts/linkerd2/values_test.go
index 44fc56c7f..a110d41cb 100644
--- a/pkg/charts/linkerd2/values_test.go
+++ b/pkg/charts/linkerd2/values_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 func TestNewValues(t *testing.T) {
-	actual, err := NewValues(false)
+	actual, err := NewValues()
 	if err != nil {
 		t.Fatalf("Unexpected error: %v\n", err)
 	}
@@ -168,7 +168,7 @@ func TestNewValues(t *testing.T) {
 	}
 
 	t.Run("HA", func(t *testing.T) {
-		actual, err := NewValues(true)
+		err := MergeHAValues(actual)
 
 		if err != nil {
 			t.Fatalf("Unexpected error: %v\n", err)
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index 501c1bfae..015799c11 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -2539,7 +2539,7 @@ data:
 }
 
 func TestFetchCurrentConfiguration(t *testing.T) {
-	defaultValues, err := linkerd2.NewValues(false)
+	defaultValues, err := linkerd2.NewValues()
 
 	if err != nil {
 		t.Fatalf("Unexpected error validating options: %v", err)
diff --git a/pkg/inject/inject_test.go b/pkg/inject/inject_test.go
index 27e440bcf..aab01fd97 100644
--- a/pkg/inject/inject_test.go
+++ b/pkg/inject/inject_test.go
@@ -23,7 +23,7 @@ func TestGetOverriddenValues(t *testing.T) {
 		pullPolicy           = "Always"
 	)
 
-	testConfig, err := l5dcharts.NewValues(false)
+	testConfig, err := l5dcharts.NewValues()
 	if err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
@@ -70,7 +70,7 @@ func TestGetOverriddenValues(t *testing.T) {
 				},
 			},
 			expected: func() *l5dcharts.Values {
-				values, _ := l5dcharts.NewValues(false)
+				values, _ := l5dcharts.NewValues()
 
 				values.GetGlobal().Proxy.Cores = 2
 				values.GetGlobal().Proxy.DisableIdentity = true
@@ -116,7 +116,7 @@ func TestGetOverriddenValues(t *testing.T) {
 				},
 			},
 			expected: func() *l5dcharts.Values {
-				values, _ := l5dcharts.NewValues(false)
+				values, _ := l5dcharts.NewValues()
 				return values
 			},
 		},
@@ -152,7 +152,7 @@ func TestGetOverriddenValues(t *testing.T) {
 				},
 			},
 			expected: func() *l5dcharts.Values {
-				values, _ := l5dcharts.NewValues(false)
+				values, _ := l5dcharts.NewValues()
 
 				values.GetGlobal().Proxy.Cores = 2
 				values.GetGlobal().Proxy.DisableIdentity = true
@@ -200,7 +200,7 @@ func TestGetOverriddenValues(t *testing.T) {
 				},
 			},
 			expected: func() *l5dcharts.Values {
-				values, _ := l5dcharts.NewValues(false)
+				values, _ := l5dcharts.NewValues()
 				return values
 			},
 		},
@@ -217,7 +217,7 @@ func TestGetOverriddenValues(t *testing.T) {
 				},
 			},
 			expected: func() *l5dcharts.Values {
-				values, _ := l5dcharts.NewValues(false)
+				values, _ := l5dcharts.NewValues()
 				values.GetGlobal().Proxy.OutboundConnectTimeout = "6005ms"
 				values.GetGlobal().Proxy.InboundConnectTimeout = "2005ms"
 				return values
@@ -247,7 +247,7 @@ func TestGetOverriddenValues(t *testing.T) {
 				},
 			},
 			expected: func() *l5dcharts.Values {
-				values, _ := l5dcharts.NewValues(false)
+				values, _ := l5dcharts.NewValues()
 				values.GetGlobal().Proxy.OpaquePorts = "3306"
 				return values
 			},
diff --git a/test/integration/opaqueports/opaque_ports_test.go b/test/integration/opaqueports/opaque_ports_test.go
new file mode 100644
index 000000000..42ac8a1fe
--- /dev/null
+++ b/test/integration/opaqueports/opaque_ports_test.go
@@ -0,0 +1,95 @@
+package opaqueports
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/linkerd/linkerd2/testutil"
+	v1 "k8s.io/api/core/v1"
+)
+
+var TestHelper *testutil.TestHelper
+
+var (
+	appName = "app"
+
+	// With the app's port marked as opaque, we expect to find a single open
+	// TCP connection that is not TLS'd because the port is skipped.
+	tcpMetric = "tcp_open_total{peer=\"src\",direction=\"inbound\",tls=\"no_identity\",no_tls_reason=\"port_skipped\"} 1"
+)
+
+func TestMain(m *testing.M) {
+	TestHelper = testutil.NewTestHelper()
+	os.Exit(m.Run())
+}
+
+//////////////////////
+/// TEST EXECUTION ///
+//////////////////////
+
+func TestOpaquePorts(t *testing.T) {
+	ctx := context.Background()
+
+	opaquePortsNs := TestHelper.GetTestNamespace("opaque-ports-test")
+	err := TestHelper.CreateDataPlaneNamespaceIfNotExists(ctx, opaquePortsNs, nil)
+	if err != nil {
+		testutil.AnnotatedFatalf(t, fmt.Sprintf("failed to create %s namespace", opaquePortsNs),
+			"failed to create %s namespace: %s", opaquePortsNs, err)
+	}
+
+	out, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/opaque_ports_application.yaml")
+	if err != nil {
+		testutil.AnnotatedFatal(t, "'linkerd inject' command failed", err)
+	}
+	out, err = TestHelper.KubectlApply(out, opaquePortsNs)
+	if err != nil {
+		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
+			"'kubectl apply' command failed\n%s", out)
+	}
+
+	// Check that the test application started correctly
+	if err := TestHelper.CheckPods(ctx, opaquePortsNs, appName, 1); err != nil {
+		if rce, ok := err.(*testutil.RestartCountError); ok {
+			testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
+		} else {
+			testutil.AnnotatedError(t, "CheckPods timed-out", err)
+		}
+	}
+
+	if err := TestHelper.CheckDeployment(ctx, opaquePortsNs, appName, 1); err != nil {
+		testutil.AnnotatedErrorf(t, "CheckDeployment timed-out", "Error validating deployment [%s]:\n%s", appName, err)
+	}
+
+	t.Run("expect inbound TCP connection with no TLS for port_skipped reason", func(t *testing.T) {
+		pods, err := TestHelper.GetPods(ctx, opaquePortsNs, map[string]string{"app": appName})
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "error getting opaque ports app pods", "error getting opaque ports app pods\n%s", err)
+		}
+
+		// Wait for slow-cooker to start sending requests
+		time.Sleep(20 * time.Second)
+
+		// Get metrics for the app pod expecting to find TCP connection counters
+		metrics, err := getPodMetrics(pods[0], opaquePortsNs)
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "error getting metrics for pod", "error getting metrics for pod\n%s", err)
+		}
+		if !strings.Contains(metrics, tcpMetric) {
+			testutil.AnnotatedFatalf(t, "failed to find expected TCP metric when port is marked as opaque", "failed to find expected TCP metric when port is marked as opaque\n%s", metrics)
+		}
+	})
+}
+
+func getPodMetrics(pod v1.Pod, ns string) (string, error) {
+	podName := fmt.Sprintf("pod/%s", pod.Name)
+	cmd := []string{"metrics", "--namespace", ns, podName}
+	metrics, err := TestHelper.LinkerdRun(cmd...)
+	if err != nil {
+		return "", err
+	}
+	return metrics, nil
+}
diff --git a/test/integration/opaqueports/testdata/opaque_ports_application.yaml b/test/integration/opaqueports/testdata/opaque_ports_application.yaml
new file mode 100644
index 000000000..e1828e9cc
--- /dev/null
+++ b/test/integration/opaqueports/testdata/opaque_ports_application.yaml
@@ -0,0 +1,64 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: app
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: app
+  template:
+    metadata:
+      labels:
+        app: app
+      annotations:
+        config.linkerd.io/opaque-ports: "8080"
+    spec:
+      containers:
+      - name: app
+        image: buoyantio/bb:v0.0.6
+        args:
+        - terminus
+        - "--h1-server-port=8080"
+        - "--response-text=app"
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: svc
+  labels:
+    app: svc
+spec:
+  selector:
+    app: app
+  ports:
+  - name: http
+    port: 8080
+    targetPort: 8080
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: slow-cooker
+spec:
+  template:
+    metadata:
+      labels:
+        app: slow-cooker
+    spec:
+      containers:
+      - name: slow-cooker
+        image: buoyantio/slow_cooker:1.3.0
+        command:
+        - "/bin/sh"
+        args:
+        - "-c"
+        - |
+          sleep 15 # wait for pods to start
+          /slow_cooker/slow_cooker -qps 1 -metric-addr 0.0.0.0:9999 http://svc:8080
+        ports:
+        - containerPort: 9999
+      restartPolicy: OnFailure