Repository: linkerd/linkerd2
Pull request #7346 (2021-11-24 03:27:13)

build(deps): bump github.com/containernetworking/cni from 0.8.1 to 1.0.1
Bumps [github.com/containernetworking/cni](https://github.com/containernetworking/cni) from 0.8.1 to 1.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/containernetworking/cni/releases">github.com/containernetworking/cni's releases</a>.</em></p>
<blockquote>
<h2>🎉 CNI v1.0.1 🎉</h2>
<p>Hot off the presses, it's CNI v1.0!</p>
<p>CNI v1.0 doesn't change much about how CNI works, but it declares API stability.</p>
<p>This release includes</p>
<ul>
<li>a rewritten spec (for clarity)</li>
<li>libcni improvements</li>
<li>way more tests.</li>
</ul>
<p>You can see the full spec changelog here: <a href="https://github.com/containernetworking/cni/blob/v1.0.1/Documentation/spec-upgrades.md">https://github.com/containernetworking/cni/blob/v1.0.1/Documentation/spec-upgrades.md</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/containernetworking/cni/commit/1694fd7b57e0176a98a12823a5ffc03337fdc152"><code>1694fd7</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/containernetworking/cni/issues/858">#858</a> from squeed/version-from</li>
<li><a href="https://github.com/containernetworking/cni/commit/c7f5f70554d026e7a3bc5a0ab52280576a2379d1"><code>c7f5f70</code></a> pkg/version: add VersionsFrom function</li>
<li><a href="https://github.com/containernetworking/cni/commit/5608690f77380743f29bb314458da9a9fb88c003"><code>5608690</code></a> spec: bump to 1.0.0</li>
<li><a href="https://github.com/containernetworking/cni/commit/699ad28878ee963ec0fbf98d276b912bf7f02a74"><code>699ad28</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/containernetworking/cni/issues/853">#853</a> from squeed/bump-spec-upgrades</li>
<li><a href="https://github.com/containernetworking/cni/commit/30e06a8b76bdfde0f84bef5da86b0bd16b1c4a0b"><code>30e06a8</code></a> Docs: bump spec version information</li>
<li><a href="https://github.com/containernetworking/cni/commit/0dfe02421ec61d736a69f803b1b8cc667e40a179"><code>0dfe024</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/containernetworking/cni/issues/851">#851</a> from DrmagicE/master</li>
<li><a href="https://github.com/containernetworking/cni/commit/a9562466f1897c6e50ad8e3d010532643c139925"><code>a956246</code></a> docs: revise cnitool docs</li>
<li><a href="https://github.com/containernetworking/cni/commit/63a3bca188021af939767520d5c87d6342dee095"><code>63a3bca</code></a> wrap returned errors</li>
<li><a href="https://github.com/containernetworking/cni/commit/b277ec1bc29ced684ae4b99c7fb742a105ee58aa"><code>b277ec1</code></a> docs: correct the extension name of the configuration file</li>
<li><a href="https://github.com/containernetworking/cni/commit/34a8a464e19ad8e2bead6ba8e43400655324b7d2"><code>34a8a46</code></a> chore: standardize documentation on IP assignment</li>
<li>Additional commits viewable in <a href="https://github.com/containernetworking/cni/compare/v0.8.1...v1.0.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/containernetworking/cni&package-manager=go_modules&previous-version=0.8.1&new-version=1.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4233bfde0..e659d28be 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -84,7 +84,7 @@ jobs:
         echo "TAG=$(CI_FORCE_CLEAN=1 bin/root-tag)" >> $GITHUB_ENV
         echo "DOCKER_BUILDKIT_CACHE=${{ runner.temp }}/.buildx-cache" >> $GITHUB_ENV
     - name: Cache docker layers
-      uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353
+      uses: actions/cache@937d24475381cd9c75ae6db12cb4e79714b926ed
       with:
         path: ${{ env.DOCKER_BUILDKIT_CACHE }}
         key: policy-controller-${{ matrix.arch }}-${{ env.TAG }}
diff --git a/.proxy-version b/.proxy-version
index e45ade6d8..54d800a40 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.167.0
+v2.168.0
diff --git a/CHANGES.md b/CHANGES.md
index 178114bfb..399bef767 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,28 @@
 # Changes
 
+## edge-21.11.4
+
+This edge release introduces a change in the destination service to honor
+opaque ports set in the `proxyProtocol` field of `Server` resources. This
+change makes it possible to set opaque ports directly in `Server` resources
+without needing the opaque ports annotation on pods. The release also features
+a number of fixes and improvements, a big thank you to our external
+contributors for their continued support and involvement.
+
+* Added support in the destination service for honoring opaque ports marked in
+  `Server` resources; ports can now be marked as opaque directly in `Server`
+  resources through the `proxyProtocol` field.
+* Added support to override default behavior and run `proxyInit` as root
+  (thanks @alex-berger!)
+* Added multicluster `Link` CRD to code generation script; consumers of the
+  multicluster API can now use a typed API to interact with multicluster links
+  (thanks @zaharidichev!)
+* Added a multicluster integration test for exported headless services (thanks
+  @importhuman!)
+* Deprecated `v1alpha1` version of the policy APIs
+* Removed newline from `linkerd check` header text (thanks @mikutas!)
+* Replaced deprecated `beta.kubernetes.io/os` label with `kubernetes.io/os`
+
 ## edge-21.11.3
 
 This edge releases fixes a compatibility issue that prevented the policy
diff --git a/Cargo.lock b/Cargo.lock
index 5dd1f2eef..2ce34ceda 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1464,9 +1464,9 @@ dependencies = [
 
 [[package]]
 name = "schemars"
-version = "0.8.7"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271ac0c667b8229adf70f0f957697c96fafd7486ab7481e15dc5e45e3e6a4368"
+checksum = "c6b5a3c80cea1ab61f4260238409510e814e38b4b563c06044edf91e7dc070e3"
 dependencies = [
  "dyn-clone",
  "schemars_derive",
@@ -1476,9 +1476,9 @@ dependencies = [
 
 [[package]]
 name = "schemars_derive"
-version = "0.8.7"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ebda811090b257411540779860bc09bf321bc587f58d2c5864309d1566214e7"
+checksum = "41ae4dce13e8614c46ac3c38ef1c0d668b101df6ac39817aebdaa26642ddae9b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1574,9 +1574,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.71"
+version = "1.0.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
+checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
 dependencies = [
  "indexmap",
  "itoa",
diff --git a/charts/linkerd2/README.md b/charts/linkerd2/README.md
index 47a4dd6d6..15a933fa6 100644
--- a/charts/linkerd2/README.md
+++ b/charts/linkerd2/README.md
@@ -219,6 +219,7 @@ Kubernetes: `>=1.20.0-0`
 | proxyInit.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the proxy-init container requests |
 | proxyInit.resources.memory.limit | string | `"50Mi"` | Maximum amount of memory that the proxy-init container can use |
 | proxyInit.resources.memory.request | string | `"10Mi"` | Amount of memory that the proxy-init container requests |
+| proxyInit.runAsRoot | bool | `false` | Allow overriding the runAsNonRoot behaviour (<https://github.com/linkerd/linkerd2/issues/7308>) |
 | proxyInit.xtMountPath.mountPath | string | `"/run"` |  |
 | proxyInit.xtMountPath.name | string | `"linkerd-proxy-init-xtables-lock"` |  |
 | proxyInjector.caBundle | string | `""` | Bundle of CA certificates for proxy injector. If not provided then Helm will use the certificate generated  for `proxyInjector.crtPEM`. If `proxyInjector.externalSecret` is set to true, this value must be set, as no certificate will be generated. |
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index 747b6d5a2..66a55adb5 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -198,6 +198,8 @@ proxyInit:
       # -- Amount of ephemeral storage that the proxy-init container requests
       request: ""
   closeWaitTimeoutSecs: 0
+  # -- Allow overriding the runAsNonRoot behaviour (<https://github.com/linkerd/linkerd2/issues/7308>)
+  runAsRoot: false
   xtMountPath:
     mountPath: /run
     name: linkerd-proxy-init-xtables-lock
diff --git a/charts/partials/templates/_proxy-init.tpl b/charts/partials/templates/_proxy-init.tpl
index f037a0038..baa5f5dd6 100644
--- a/charts/partials/templates/_proxy-init.tpl
+++ b/charts/partials/templates/_proxy-init.tpl
@@ -29,7 +29,7 @@ imagePullPolicy: {{.Values.proxyInit.image.pullPolicy | default .Values.imagePul
 name: linkerd-init
 {{ include "partials.resources" .Values.proxyInit.resources }}
 securityContext:
-  {{- if .Values.proxyInit.closeWaitTimeoutSecs }}
+  {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.runAsRoot }}
   allowPrivilegeEscalation: true
   {{- else }}
   allowPrivilegeEscalation: false
@@ -46,7 +46,7 @@ securityContext:
     {{- include "partials.proxy-init.capabilities.drop" . | nindent 4 -}}
     {{- end }}
     {{- end }}
-  {{- if .Values.proxyInit.closeWaitTimeoutSecs }}
+  {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.runAsRoot }}
   privileged: true
   runAsNonRoot: false
   runAsUser: 0
diff --git a/cli/cmd/check.go b/cli/cmd/check.go
index cd787e63e..1703b10d8 100644
--- a/cli/cmd/check.go
+++ b/cli/cmd/check.go
@@ -195,6 +195,7 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, s
 	}
 
 	hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
+		IsMainCheckCommand:    true,
 		ControlPlaneNamespace: controlPlaneNamespace,
 		CNINamespace:          cniNamespace,
 		DataPlaneNamespace:    options.namespace,
@@ -209,10 +210,9 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, s
 		InstallManifest:       installManifest,
 	})
 
-	if options.output != jsonOutput {
-		healthcheck.PrintCoreChecksHeader(wout)
+	if options.output == tableOutput {
+		healthcheck.PrintChecksHeader(wout, true)
 	}
-
 	success := healthcheck.RunChecks(wout, werr, hc, options.output)
 
 	extensionSuccess, err := runExtensionChecks(cmd, wout, werr, options)
@@ -222,7 +222,10 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, s
 		os.Exit(1)
 	}
 
-	if !success || !extensionSuccess {
+	totalSuccess := success && extensionSuccess
+	healthcheck.PrintChecksResult(wout, options.output, totalSuccess)
+
+	if !totalSuccess {
 		os.Exit(1)
 	}
 
diff --git a/cni-plugin/main.go b/cni-plugin/main.go
index e9943bf75..bcc23c357 100644
--- a/cni-plugin/main.go
+++ b/cni-plugin/main.go
@@ -27,7 +27,7 @@ import (
 
 	"github.com/containernetworking/cni/pkg/skel"
 	"github.com/containernetworking/cni/pkg/types"
-	"github.com/containernetworking/cni/pkg/types/current"
+	cniv1 "github.com/containernetworking/cni/pkg/types/100"
 	"github.com/containernetworking/cni/pkg/version"
 	"github.com/linkerd/linkerd2-proxy-init/cmd"
 	"github.com/linkerd/linkerd2-proxy-init/iptables"
@@ -70,7 +70,7 @@ type PluginConf struct {
 	// This is the previous result, when called in the context of a chained
 	// plugin. We will just pass any prevResult through.
 	RawPrevResult *map[string]interface{} `json:"prevResult"`
-	PrevResult    *current.Result         `json:"-"`
+	PrevResult    *cniv1.Result           `json:"-"`
 
 	LogLevel   string     `json:"log_level"`
 	ProxyInit  ProxyInit  `json:"linkerd"`
@@ -115,9 +115,9 @@ func parseConfig(stdin []byte) (*PluginConf, error) {
 			return nil, fmt.Errorf("linkerd-cni: could not parse prevResult: %v", err)
 		}
 		conf.RawPrevResult = nil
-		conf.PrevResult, err = current.NewResultFromResult(res)
+		conf.PrevResult, err = cniv1.NewResultFromResult(res)
 		if err != nil {
-			return nil, fmt.Errorf("linkerd-cni: could not convert result to current version: %v", err)
+			return nil, fmt.Errorf("linkerd-cni: could not convert result to version 1.0: %v", err)
 		}
 		logrus.Debugf("linkerd-cni: prevResult: %v", conf.PrevResult)
 	}
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index 3dcec22aa..40e1f69c7 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -2,6 +2,7 @@ package destination
 
 import (
 	"fmt"
+	"reflect"
 	"strconv"
 	"strings"
 
@@ -163,9 +164,14 @@ func (et *endpointTranslator) diffEndpoints(filtered watcher.AddressSet) (watche
 	add := make(map[watcher.ID]watcher.Address)
 	remove := make(map[watcher.ID]watcher.Address)
 
-	for id, address := range filtered.Addresses {
-		if _, ok := et.filteredSnapshot.Addresses[id]; !ok {
-			add[id] = address
+	for id, new := range filtered.Addresses {
+		old, ok := et.filteredSnapshot.Addresses[id]
+		if !ok {
+			add[id] = new
+		} else {
+			if !reflect.DeepEqual(old, new) {
+				add[id] = new
+			}
 		}
 	}
 
@@ -209,26 +215,16 @@ func (et *endpointTranslator) sendClientAdd(set watcher.AddressSet) {
 	addrs := []*pb.WeightedAddr{}
 	for _, address := range set.Addresses {
 		var (
-			wa  *pb.WeightedAddr
-			err error
+			wa          *pb.WeightedAddr
+			opaquePorts map[uint32]struct{}
+			err         error
 		)
 		if address.Pod != nil {
-			opaquePorts, ok, getErr := getPodOpaquePortsAnnotations(address.Pod)
-			if getErr != nil {
-				et.log.Errorf("failed getting opaque ports annotation for pod: %s", getErr)
-			}
-			// If the opaque ports annotation was not set, then set the
-			// endpoint's opaque ports to the default value.
-			if !ok {
-				opaquePorts = et.defaultOpaquePorts
-			}
-
-			skippedInboundPorts, skippedErr := getPodSkippedInboundPortsAnnotations(address.Pod)
-			if skippedErr != nil {
-				et.log.Errorf("failed getting ignored inbound ports annotation for pod: %s", err)
+			opaquePorts, err = getPodOpaquePorts(address.Pod, et.defaultOpaquePorts)
+			if err != nil {
+				et.log.Errorf("failed to get opaque ports for pod %s/%s: %s", address.Pod.Namespace, address.Pod.Name, err)
 			}
-
-			wa, err = toWeightedAddr(address, opaquePorts, skippedInboundPorts, et.enableH2Upgrade, et.identityTrustDomain, et.controllerNS, et.log)
+			wa, err = toWeightedAddr(address, opaquePorts, et.enableH2Upgrade, et.identityTrustDomain, et.controllerNS, et.log)
 		} else {
 			var authOverride *pb.AuthorityOverride
 			if address.AuthorityOverride != "" {
@@ -318,11 +314,22 @@ func toAddr(address watcher.Address) (*net.TcpAddress, error) {
 	}, nil
 }
 
-func toWeightedAddr(address watcher.Address, opaquePorts, skippedInboundPorts map[uint32]struct{}, enableH2Upgrade bool, identityTrustDomain string, controllerNS string, log *logging.Entry) (*pb.WeightedAddr, error) {
+func toWeightedAddr(address watcher.Address, opaquePorts map[uint32]struct{}, enableH2Upgrade bool, identityTrustDomain string, controllerNS string, log *logging.Entry) (*pb.WeightedAddr, error) {
+	// When converting an address to a weighted addr, it should be backed by a Pod.
+	if address.Pod == nil {
+		return nil, fmt.Errorf("endpoint not backed by Pod: %s:%d", address.IP, address.Port)
+	}
+
+	skippedInboundPorts, err := getPodSkippedInboundPortsAnnotations(address.Pod)
+	if err != nil {
+		log.Errorf("failed to get ignored inbound ports annotation for pod: %s", err)
+	}
+
 	controllerNSLabel := address.Pod.Labels[k8s.ControllerNSLabel]
 	sa, ns := k8s.GetServiceAccountAndNS(address.Pod)
 	labels := k8s.GetPodLabels(address.OwnerKind, address.OwnerName, address.Pod)
 	_, isSkippedInboundPort := skippedInboundPorts[address.Port]
+
 	// If the pod is controlled by any Linkerd control plane, then it can be
 	// hinted that this destination knows H2 (and handles our orig-proto
 	// translation)
@@ -333,7 +340,11 @@ func toWeightedAddr(address watcher.Address, opaquePorts, skippedInboundPorts ma
 				H2: &pb.ProtocolHint_H2{},
 			}
 		}
-		if _, ok := opaquePorts[address.Port]; ok {
+		// If address is set as opaque by a Server, or its port is set as
+		// opaque by annotation or default value, then hint its proxy's
+		// inbound port.
+		_, opaquePort := opaquePorts[address.Port]
+		if address.OpaqueProtocol || opaquePort {
 			port, err := getInboundPort(&address.Pod.Spec)
 			if err != nil {
 				log.Error(err)
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index 2f89091ef..59b98da8d 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -345,45 +345,36 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 // include an endpoint. Otherwise, the default profile is sent.
 func (s *server) sendEndpointProfile(stream pb.Destination_GetProfileServer, pod *corev1.Pod, port uint32) error {
 	log := s.log
-	var endpoint *pb.WeightedAddr
+	var weightedAddr *pb.WeightedAddr
 	opaquePorts := make(map[uint32]struct{})
 	var err error
 	if pod != nil {
-		podSet := podToAddressSet(s.k8sAPI, pod).WithPort(port)
-		podID := watcher.PodID{
-			Namespace: pod.Namespace,
-			Name:      pod.Name,
+		ownerKind, ownerName := s.k8sAPI.GetOwnerKindAndName(context.Background(), pod, true)
+		address := watcher.Address{
+			IP:        pod.Status.PodIP,
+			Port:      port,
+			Pod:       pod,
+			OwnerName: ownerName,
+			OwnerKind: ownerKind,
 		}
-		var ok bool
-		opaquePorts, ok, err = getPodOpaquePortsAnnotations(pod)
+		opaquePorts, err = getPodOpaquePorts(pod, s.defaultOpaquePorts)
 		if err != nil {
-			log.Errorf("failed to get opaque ports annotation for pod: %s", err)
+			log.Errorf("failed to get opaque ports for pod %s/%s: %s", pod.Namespace, pod.Name, err)
 		}
-
-		// If the opaque ports annotation was not set, then set the
-		// endpoint's opaque ports to the default value.
-		if !ok {
-			opaquePorts = s.defaultOpaquePorts
-		}
-
-		skippedInboundPorts, err := getPodSkippedInboundPortsAnnotations(pod)
-		if err != nil {
-			log.Errorf("failed to get ignored inbound ports annotation for pod: %s", err)
-		}
-
-		endpoint, err = toWeightedAddr(podSet.Addresses[podID], opaquePorts, skippedInboundPorts, s.enableH2Upgrade, s.identityTrustDomain, s.controllerNS, s.log)
+		weightedAddr, err = toWeightedAddr(address, opaquePorts, s.enableH2Upgrade, s.identityTrustDomain, s.controllerNS, s.log)
 		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
+		weightedAddr.MetricLabels["namespace"] = pod.Namespace
 	}
 
 	// Send the default profile without subscribing for future updates. The
 	// profile response will also include an endpoint if the IP (or hostname)
 	// sent in the profile request maps to a pod.
-	translator := newProfileTranslator(stream, log, "", port, endpoint)
+	translator := newProfileTranslator(stream, log, "", port, weightedAddr)
 
 	// If there are opaque ports then update the profile translator
 	// with a service profile that has those values
@@ -527,26 +518,6 @@ func podReceivingTraffic(pod *corev1.Pod) bool {
 	return !podTerminating && !podTerminated
 }
 
-// podToAddressSet converts a Pod spec into a set of Addresses.
-func podToAddressSet(k8sAPI *k8s.API, pod *corev1.Pod) *watcher.AddressSet {
-	ownerKind, ownerName := k8sAPI.GetOwnerKindAndName(context.Background(), pod, true)
-	return &watcher.AddressSet{
-		Addresses: map[watcher.PodID]watcher.Address{
-			{
-				Name:      pod.Name,
-				Namespace: pod.Namespace,
-			}: {
-				IP:        pod.Status.PodIP,
-				Port:      0, // Will be set by individual subscriptions
-				Pod:       pod,
-				OwnerName: ownerName,
-				OwnerKind: ownerKind,
-			},
-		},
-		Labels: map[string]string{"namespace": pod.Namespace},
-	}
-}
-
 ////////////
 /// util ///
 ////////////
@@ -660,22 +631,22 @@ func hasSuffix(slice []string, suffix []string) bool {
 	return true
 }
 
-func getPodOpaquePortsAnnotations(pod *corev1.Pod) (map[uint32]struct{}, bool, error) {
+func getPodOpaquePorts(pod *corev1.Pod, defaultPorts map[uint32]struct{}) (map[uint32]struct{}, error) {
 	annotation, ok := pod.Annotations[labels.ProxyOpaquePortsAnnotation]
 	if !ok {
-		return nil, false, nil
+		return defaultPorts, nil
 	}
 	opaquePorts := make(map[uint32]struct{})
 	if annotation != "" {
 		for _, portStr := range util.ParseContainerOpaquePorts(annotation, pod.Spec.Containers) {
 			port, err := strconv.ParseUint(portStr, 10, 32)
 			if err != nil {
-				return nil, true, err
+				return nil, err
 			}
 			opaquePorts[uint32(port)] = struct{}{}
 		}
 	}
-	return opaquePorts, true, nil
+	return opaquePorts, nil
 }
 
 func getPodSkippedInboundPortsAnnotations(pod *corev1.Pod) (map[uint32]struct{}, error) {
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index 7c6cfe4fe..49457432a 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"sync"
 
+	"github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	consts "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/client_golang/prometheus"
@@ -14,7 +15,8 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	discovery "k8s.io/api/discovery/v1beta1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	k8slabels "k8s.io/apimachinery/pkg/labels"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/client-go/tools/cache"
 )
@@ -28,6 +30,8 @@ const (
 	targetCluster          = "target_cluster"
 	targetService          = "target_service"
 	targetServiceNamespace = "target_service_namespace"
+
+	opaqueProtocol = "opaque"
 )
 
 const endpointTargetRefPod = "Pod"
@@ -50,6 +54,7 @@ type (
 		Identity          string
 		AuthorityOverride string
 		ForZones          []discovery.ForZone
+		OpaqueProtocol    bool
 	}
 
 	// AddressSet is a set of Address, indexed by ID.
@@ -146,6 +151,12 @@ func NewEndpointsWatcher(k8sAPI *k8s.API, log *logging.Entry, enableEndpointSlic
 		UpdateFunc: func(_, obj interface{}) { ew.addService(obj) },
 	})
 
+	k8sAPI.Srv().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    ew.addServer,
+		DeleteFunc: ew.deleteServer,
+		UpdateFunc: func(_, obj interface{}) { ew.addServer(obj) },
+	})
+
 	if ew.enableEndpointSlices {
 		ew.log.Debugf("Watching EndpointSlice resources")
 		k8sAPI.ES().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -404,6 +415,20 @@ func (ew *EndpointsWatcher) getServicePublisher(id ServiceID) (sp *servicePublis
 	return
 }
 
+func (ew *EndpointsWatcher) addServer(obj interface{}) {
+	server := obj.(*v1beta1.Server)
+	for _, sp := range ew.publishers {
+		sp.updateServer(server, true)
+	}
+}
+
+func (ew *EndpointsWatcher) deleteServer(obj interface{}) {
+	server := obj.(*v1beta1.Server)
+	for _, sp := range ew.publishers {
+		sp.updateServer(server, false)
+	}
+}
+
 ////////////////////////
 /// servicePublisher ///
 ////////////////////////
@@ -529,7 +554,7 @@ func (sp *servicePublisher) newPortPublisher(srcPort Port, hostname string) *por
 
 	if port.enableEndpointSlices {
 		matchLabels := map[string]string{discovery.LabelServiceName: sp.id.Name}
-		selector := k8slabels.Set(matchLabels).AsSelector()
+		selector := labels.Set(matchLabels).AsSelector()
 
 		sliceList, err := sp.k8sAPI.ES().Lister().EndpointSlices(sp.id.Namespace).List(selector)
 		if err != nil && !apierrors.IsNotFound(err) {
@@ -557,6 +582,17 @@ func (sp *servicePublisher) metricsLabels(port Port, hostname string) prometheus
 	return endpointsLabels(sp.id.Namespace, sp.id.Name, strconv.Itoa(int(port)), hostname)
 }
 
+func (sp *servicePublisher) updateServer(server *v1beta1.Server, isAdd bool) {
+	selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
+	if err != nil {
+		sp.log.Errorf("failed to create Selector: %s", err)
+		return
+	}
+	for _, pp := range sp.ports {
+		pp.updateServer(server, selector, isAdd)
+	}
+}
+
 /////////////////////
 /// portPublisher ///
 /////////////////////
@@ -611,10 +647,14 @@ func (pp *portPublisher) addEndpointSlice(slice *discovery.EndpointSlice) {
 
 func (pp *portPublisher) updateEndpointSlice(oldSlice *discovery.EndpointSlice, newSlice *discovery.EndpointSlice) {
 	updatedAddressSet := AddressSet{
-		Addresses: pp.addresses.Addresses,
+		Addresses: make(map[ID]Address),
 		Labels:    pp.addresses.Labels,
 	}
 
+	for id, address := range pp.addresses.Addresses {
+		updatedAddressSet.Addresses[id] = address
+	}
+
 	oldAddressSet := pp.endpointSliceToAddresses(oldSlice)
 	for id := range oldAddressSet.Addresses {
 		delete(updatedAddressSet.Addresses, id)
@@ -678,14 +718,12 @@ func metricLabels(resource interface{}) map[string]string {
 }
 
 func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) AddressSet {
-	addressSet := AddressSet{
-		Labels:    metricLabels(es),
-		Addresses: make(map[ID]Address),
-	}
-
 	resolvedPort := pp.resolveESTargetPort(es.Ports)
 	if resolvedPort == undefinedEndpointPort {
-		return addressSet
+		return AddressSet{
+			Labels:    metricLabels(es),
+			Addresses: make(map[ID]Address),
+		}
 	}
 
 	serviceID, err := getEndpointSliceServiceID(es)
@@ -693,6 +731,7 @@ func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) A
 		pp.log.Errorf("Could not fetch resource service name:%v", err)
 	}
 
+	addresses := make(map[ID]Address)
 	for _, endpoint := range es.Endpoints {
 		if endpoint.Hostname != nil {
 			if pp.hostname != "" && pp.hostname != *endpoint.Hostname {
@@ -719,10 +758,8 @@ func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) A
 					copy(zones, endpoint.Hints.ForZones)
 					address.ForZones = zones
 				}
-
-				addressSet.Addresses[id] = address
+				addresses[id] = address
 			}
-
 			continue
 		}
 
@@ -733,20 +770,25 @@ func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) A
 					pp.log.Errorf("Unable to create new address:%v", err)
 					continue
 				}
-
+				err = setToServerProtocol(pp.k8sAPI, &address, resolvedPort)
+				if err != nil {
+					pp.log.Errorf("failed to set address OpaqueProtocol: %s", err)
+					continue
+				}
 				if endpoint.Hints != nil {
 					zones := make([]discovery.ForZone, len(endpoint.Hints.ForZones))
 					copy(zones, endpoint.Hints.ForZones)
 					address.ForZones = zones
 				}
-
-				addressSet.Addresses[id] = address
+				addresses[id] = address
 			}
 		}
 
 	}
-
-	return addressSet
+	return AddressSet{
+		Addresses: addresses,
+		Labels:    metricLabels(es),
+	}
 }
 
 func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) AddressSet {
@@ -781,8 +823,10 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) Addre
 					pp.log.Errorf("Unable to create new address:%v", err)
 					continue
 				}
+				err = setToServerProtocol(pp.k8sAPI, &address, resolvedPort)
 				if err != nil {
-					pp.log.Errorf("failed to set opaque port annotation on pod: %s", err)
+					pp.log.Errorf("failed to set address OpaqueProtocol: %s", err)
+					continue
 				}
 				addresses[id] = address
 			}
@@ -869,7 +913,7 @@ func (pp *portPublisher) updatePort(targetPort namedPort) {
 
 	if pp.enableEndpointSlices {
 		matchLabels := map[string]string{discovery.LabelServiceName: pp.id.Name}
-		selector := k8slabels.Set(matchLabels).AsSelector()
+		selector := labels.Set(matchLabels).AsSelector()
 
 		endpointSlices, err := pp.k8sAPI.ES().Lister().EndpointSlices(pp.id.Namespace).List(selector)
 		if err == nil {
@@ -945,6 +989,41 @@ func (pp *portPublisher) unsubscribe(listener EndpointUpdateListener) {
 	pp.metrics.setSubscribers(len(pp.listeners))
 }
 
+func (pp *portPublisher) updateServer(server *v1beta1.Server, selector labels.Selector, isAdd bool) {
+	for id, address := range pp.addresses.Addresses {
+		if address.Pod != nil && selector.Matches(labels.Set(address.Pod.Labels)) {
+			var portMatch bool
+			switch server.Spec.Port.Type {
+			case intstr.Int:
+				if server.Spec.Port.IntVal == int32(address.Port) {
+					portMatch = true
+				}
+			case intstr.String:
+				for _, c := range address.Pod.Spec.Containers {
+					for _, p := range c.Ports {
+						if p.ContainerPort == int32(address.Port) && p.Name == server.Spec.Port.StrVal {
+							portMatch = true
+						}
+					}
+				}
+			default:
+				continue
+			}
+			if portMatch {
+				if isAdd && server.Spec.ProxyProtocol == opaqueProtocol {
+					address.OpaqueProtocol = true
+				} else {
+					address.OpaqueProtocol = false
+				}
+				pp.addresses.Addresses[id] = address
+			}
+		}
+	}
+	for _, listener := range pp.listeners {
+		listener.Add(pp.addresses)
+	}
+}
+
 ////////////
 /// util ///
 ////////////
@@ -979,7 +1058,6 @@ func getTargetPort(service *corev1.Service, port Port) namedPort {
 	// port spec's name as the target port
 	for _, portSpec := range service.Spec.Ports {
 		if portSpec.Port == int32(port) {
-
 			return intstr.FromString(portSpec.Name)
 		}
 	}
@@ -1063,3 +1141,43 @@ func isValidSlice(es *discovery.EndpointSlice) bool {
 
 	return true
 }
+
+func setToServerProtocol(k8sAPI *k8s.API, address *Address, port Port) error {
+	servers, err := k8sAPI.Srv().Lister().Servers("").List(labels.Everything())
+	if err != nil {
+		return fmt.Errorf("failed to list Servers: %s", err)
+	}
+	if address.Pod == nil {
+		return fmt.Errorf("endpoint not backed by Pod: %s:%d", address.IP, address.Port)
+	}
+	for _, server := range servers {
+		selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
+		if err != nil {
+			return fmt.Errorf("failed to create Selector: %s", err)
+		}
+		if server.Spec.ProxyProtocol == opaqueProtocol && selector.Matches(labels.Set(address.Pod.Labels)) {
+			var portMatch bool
+			switch server.Spec.Port.Type {
+			case intstr.Int:
+				if server.Spec.Port.IntVal == int32(port) {
+					portMatch = true
+				}
+			case intstr.String:
+				for _, c := range address.Pod.Spec.Containers {
+					for _, p := range c.Ports {
+						if p.ContainerPort == int32(port) && p.Name == server.Spec.Port.StrVal {
+							portMatch = true
+						}
+					}
+				}
+			default:
+				continue
+			}
+			if portMatch {
+				address.OpaqueProtocol = true
+				return nil
+			}
+		}
+	}
+	return nil
+}
diff --git a/controller/api/destination/watcher/opaque_ports_watcher.go b/controller/api/destination/watcher/opaque_ports_watcher.go
index 809bd62ca..6f553c33c 100644
--- a/controller/api/destination/watcher/opaque_ports_watcher.go
+++ b/controller/api/destination/watcher/opaque_ports_watcher.go
@@ -8,7 +8,6 @@ import (
 	"github.com/linkerd/linkerd2/controller/k8s"
 	labels "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/util"
-	log "github.com/sirupsen/logrus"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/tools/cache"
@@ -212,7 +211,7 @@ func parseServiceOpaquePorts(annotation string, sps []corev1.ServicePort) []stri
 		} else {
 			pr, err := ports.ParsePortRange(pr)
 			if err != nil {
-				log.Warnf("Invalid port range [%v]: %s", pr, err)
+				logging.Warnf("Invalid port range [%v]: %s", pr, err)
 				continue
 			}
 			for i := pr.LowerBound; i <= pr.UpperBound; i++ {
diff --git a/controller/cmd/destination/main.go b/controller/cmd/destination/main.go
index d0ab57c85..302a747b7 100644
--- a/controller/cmd/destination/main.go
+++ b/controller/cmd/destination/main.go
@@ -94,14 +94,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.NS, k8s.Node,
+			k8s.Endpoint, k8s.ES, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS, k8s.Node, k8s.Srv,
 		)
 	} else {
 		k8sAPI, err = k8s.InitializeAPI(
 			ctx,
 			*kubeConfigPath,
 			true,
-			k8s.Endpoint, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS, k8s.Node,
+			k8s.Endpoint, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job, k8s.NS, k8s.Node, k8s.Srv,
 		)
 	}
 	if err != nil {
diff --git a/controller/heartbeat/heartbeat.go b/controller/heartbeat/heartbeat.go
index 78b2420e6..4c9324755 100644
--- a/controller/heartbeat/heartbeat.go
+++ b/controller/heartbeat/heartbeat.go
@@ -60,13 +60,13 @@ func K8sValues(ctx context.Context, kubeAPI *k8s.KubernetesAPI, controlPlaneName
 		return v
 	}
 
-	spClient, err := pkgK8s.NewSpClientSet(kubeAPI.Config)
+	l5dCrdClient, err := pkgK8s.NewL5DCRDClient(kubeAPI.Config)
 	if err != nil {
-		log.Errorf("Failed to create service profile client: %s", err)
+		log.Errorf("Failed to create Linkerd CRD client: %s", err)
 		return v
 	}
 
-	spList, err := spClient.LinkerdV1alpha2().ServiceProfiles("").List(ctx, v1.ListOptions{})
+	spList, err := l5dCrdClient.LinkerdV1alpha2().ServiceProfiles("").List(ctx, v1.ListOptions{})
 	if err != nil {
 		log.Errorf("Failed to get service profiles: %s", err)
 		return v
diff --git a/controller/k8s/api.go b/controller/k8s/api.go
index 82cf4556a..64d6b8596 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -10,8 +10,9 @@ import (
 	"k8s.io/client-go/rest"
 
 	spv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
-	spclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
-	sp "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions"
+	l5dcrdclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	l5dcrdinformer "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions"
+	srvinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/server/v1beta1"
 	spinformers "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/serviceprofile/v1alpha2"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/client_golang/prometheus"
@@ -65,6 +66,8 @@ const (
 	Node
 	Secret
 	ES // EndpointSlice resource
+	Srv
+	Saz
 )
 
 // API provides shared informers for all Kubernetes objects
@@ -90,11 +93,12 @@ type API struct {
 	ts       tsinformers.TrafficSplitInformer
 	node     coreinformers.NodeInformer
 	secret   coreinformers.SecretInformer
+	srv      srvinformers.ServerInformer
 
-	syncChecks        []cache.InformerSynced
-	sharedInformers   informers.SharedInformerFactory
-	spSharedInformers sp.SharedInformerFactory
-	tsSharedInformers ts.SharedInformerFactory
+	syncChecks            []cache.InformerSynced
+	sharedInformers       informers.SharedInformerFactory
+	l5dCrdSharedInformers l5dcrdinformer.SharedInformerFactory
+	tsSharedInformers     ts.SharedInformerFactory
 
 	gauges []prometheus.GaugeFunc
 }
@@ -140,22 +144,28 @@ func initAPI(ctx context.Context, k8sClient *k8s.KubernetesAPI, dynamicClient dy
 		}
 	}
 
-	// check for need and access to ServiceProfiles
-	var spClient *spclient.Clientset
+	// check for need and access to Linkerd CRD clients
+	var l5dCrdClient *l5dcrdclient.Clientset
 	for _, res := range resources {
-		if res == SP {
+		switch {
+		case res == SP:
 			err := k8s.ServiceProfilesAccess(ctx, k8sClient)
 			if err != nil {
 				return nil, err
 			}
-
-			spClient, err = NewSpClientSet(kubeConfig)
+		case res == Srv:
+			err := k8s.ServersAccess(ctx, k8sClient)
 			if err != nil {
 				return nil, err
 			}
-
-			break
+		default:
+			continue
 		}
+		l5dCrdClient, err = NewL5DCRDClient(kubeConfig)
+		if err != nil {
+			return nil, err
+		}
+		break
 	}
 
 	// TrafficSplits
@@ -171,7 +181,7 @@ func initAPI(ctx context.Context, k8sClient *k8s.KubernetesAPI, dynamicClient dy
 		}
 	}
 
-	api := NewAPI(k8sClient, dynamicClient, spClient, tsClient, resources...)
+	api := NewAPI(k8sClient, dynamicClient, l5dCrdClient, tsClient, resources...)
 	for _, gauge := range api.gauges {
 		prometheus.Register(gauge)
 	}
@@ -182,15 +192,15 @@ func initAPI(ctx context.Context, k8sClient *k8s.KubernetesAPI, dynamicClient dy
 func NewAPI(
 	k8sClient kubernetes.Interface,
 	dynamicClient dynamic.Interface,
-	spClient spclient.Interface,
+	l5dCrdClient l5dcrdclient.Interface,
 	tsClient tsclient.Interface,
 	resources ...APIResource,
 ) *API {
 	sharedInformers := informers.NewSharedInformerFactory(k8sClient, 10*time.Minute)
 
-	var spSharedInformers sp.SharedInformerFactory
-	if spClient != nil {
-		spSharedInformers = sp.NewSharedInformerFactory(spClient, 10*time.Minute)
+	var l5dCrdSharedInformers l5dcrdinformer.SharedInformerFactory
+	if l5dCrdClient != nil {
+		l5dCrdSharedInformers = l5dcrdinformer.NewSharedInformerFactory(l5dCrdClient, 10*time.Minute)
 	}
 
 	var tsSharedInformers ts.SharedInformerFactory
@@ -199,12 +209,12 @@ func NewAPI(
 	}
 
 	api := &API{
-		Client:            k8sClient,
-		DynamicClient:     dynamicClient,
-		syncChecks:        make([]cache.InformerSynced, 0),
-		sharedInformers:   sharedInformers,
-		spSharedInformers: spSharedInformers,
-		tsSharedInformers: tsSharedInformers,
+		Client:                k8sClient,
+		DynamicClient:         dynamicClient,
+		syncChecks:            make([]cache.InformerSynced, 0),
+		sharedInformers:       sharedInformers,
+		l5dCrdSharedInformers: l5dCrdSharedInformers,
+		tsSharedInformers:     tsSharedInformers,
 	}
 
 	for _, resource := range resources {
@@ -258,12 +268,19 @@ func NewAPI(
 			api.syncChecks = append(api.syncChecks, api.rs.Informer().HasSynced)
 			api.addInformerSizeGauge("replica_set", api.rs.Informer())
 		case SP:
-			if spSharedInformers == nil {
-				panic("SP shared informer not configured")
+			if l5dCrdSharedInformers == nil {
+				panic("Linkerd CRD shared informer not configured")
 			}
-			api.sp = spSharedInformers.Linkerd().V1alpha2().ServiceProfiles()
+			api.sp = l5dCrdSharedInformers.Linkerd().V1alpha2().ServiceProfiles()
 			api.syncChecks = append(api.syncChecks, api.sp.Informer().HasSynced)
 			api.addInformerSizeGauge("service_profile", api.sp.Informer())
+		case Srv:
+			if l5dCrdSharedInformers == nil {
+				panic("Linkerd CRD shared informer not configured")
+			}
+			api.srv = l5dCrdSharedInformers.Server().V1beta1().Servers()
+			api.syncChecks = append(api.syncChecks, api.srv.Informer().HasSynced)
+			api.addInformerSizeGauge("server", api.srv.Informer())
 		case SS:
 			api.ss = sharedInformers.Apps().V1().StatefulSets()
 			api.syncChecks = append(api.syncChecks, api.ss.Informer().HasSynced)
@@ -296,8 +313,8 @@ func NewAPI(
 func (api *API) Sync(stopCh <-chan struct{}) {
 	api.sharedInformers.Start(stopCh)
 
-	if api.spSharedInformers != nil {
-		api.spSharedInformers.Start(stopCh)
+	if api.l5dCrdSharedInformers != nil {
+		api.l5dCrdSharedInformers.Start(stopCh)
 	}
 
 	if api.tsSharedInformers != nil {
@@ -411,6 +428,14 @@ func (api *API) SP() spinformers.ServiceProfileInformer {
 	return api.sp
 }
 
+// Srv provides access to a shared informer and lister for Servers.
+func (api *API) Srv() srvinformers.ServerInformer {
+	if api.srv == nil {
+		panic("Srv informer not configured")
+	}
+	return api.srv
+}
+
 // MWC provides access to a shared informer and lister for MutatingWebhookConfigurations.
 func (api *API) MWC() arinformers.MutatingWebhookConfigurationInformer {
 	if api.mwc == nil {
diff --git a/controller/k8s/clientset.go b/controller/k8s/clientset.go
index 4a1afc680..01b4f826c 100644
--- a/controller/k8s/clientset.go
+++ b/controller/k8s/clientset.go
@@ -1,7 +1,7 @@
 package k8s
 
 import (
-	spclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
+	l5dcrdclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
 	"github.com/linkerd/linkerd2/pkg/prometheus"
 	tsclient "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned"
 	"k8s.io/client-go/rest"
@@ -16,10 +16,10 @@ func wrapTransport(config *rest.Config, telemetryName string) *rest.Config {
 	return config
 }
 
-// NewSpClientSet returns a Kubernetes ServiceProfile client for the given
+// NewL5DCRDClient returns a Linkerd controller client for the given
 // configuration.
-func NewSpClientSet(kubeConfig *rest.Config) (*spclient.Clientset, error) {
-	return spclient.NewForConfig(wrapTransport(kubeConfig, "sp"))
+func NewL5DCRDClient(kubeConfig *rest.Config) (*l5dcrdclient.Clientset, error) {
+	return l5dcrdclient.NewForConfig(wrapTransport(kubeConfig, "l5dCrd"))
 }
 
 // NewTsClientSet returns a Kubernetes TrafficSplit client for the given
diff --git a/controller/script/destination-client/main.go b/controller/script/destination-client/main.go
index 52aab54fc..3bf28e54d 100644
--- a/controller/script/destination-client/main.go
+++ b/controller/script/destination-client/main.go
@@ -69,6 +69,9 @@ func get(client pb.DestinationClient, req *pb.GetDestination) {
 					log.Printf("  - protocol hint: UNKNOWN")
 				}
 				log.Printf("  - identity: %s", addr.GetTlsIdentity())
+				if ot := addr.GetProtocolHint().GetOpaqueTransport(); ot != nil {
+					log.Printf("  - opaque transport port: %d", ot.GetInboundPort())
+				}
 			}
 			log.Println()
 		case *pb.Update_Remove:
diff --git a/go.mod b/go.mod
index f6ceb719a..8118ac3d4 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	contrib.go.opencensus.io/exporter/ocagent v0.7.0
 	github.com/briandowns/spinner v0.0.0-20190212173954-5cf08d0ac778
 	github.com/clarketm/json v1.15.7
-	github.com/containernetworking/cni v0.8.1
+	github.com/containernetworking/cni v1.0.1
 	github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect
 	github.com/emicklei/proto v1.9.1
 	github.com/evanphx/json-patch v4.11.0+incompatible
diff --git a/go.sum b/go.sum
index 373e948b7..9ce766b32 100644
--- a/go.sum
+++ b/go.sum
@@ -275,8 +275,9 @@ github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1Dv
 github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
 github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
 github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
-github.com/containernetworking/cni v0.8.1 h1:7zpDnQ3T3s4ucOuJ/ZCLrYBxzkg0AELFfII3Epo9TmI=
 github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/cni v1.0.1 h1:9OIL/sZmMYDBe+G8svzILAlulUpaDTUjeAbtH/JNLBo=
+github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y=
 github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=
 github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8=
 github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=
@@ -755,6 +756,7 @@ github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
 github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
 github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
@@ -857,6 +859,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
@@ -1519,7 +1522,6 @@ k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
 k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
 k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
 k8s.io/api v0.22.1/go.mod h1:bh13rkTp3F1XEaLGykbyRD2QaTTzPm0e/BMd8ptFONY=
-k8s.io/api v0.22.3/go.mod h1:azgiXFiXqiWyLCfI62/eYBOu19rj2LKmIhFPP4+33fs=
 k8s.io/api v0.22.4 h1:UvyHW0ezB2oIgHAxlYoo6UJQObYXU7awuNarwoHEOjw=
 k8s.io/api v0.22.4/go.mod h1:Rgs+9gIGYC5laXQSZZ9JqT5NevNgoGiOdVWi1BAB3qk=
 k8s.io/apiextensions-apiserver v0.22.1/go.mod h1:HeGmorjtRmRLE+Q8dJu6AYRoZccvCMsghwS8XTUYb2c=
@@ -1530,14 +1532,12 @@ k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRp
 k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
 k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
 k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=
-k8s.io/apimachinery v0.22.3/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=
 k8s.io/apimachinery v0.22.4 h1:9uwcvPpukBw/Ri0EUmWz+49cnFtaoiyEhQTK+xOe7Ck=
 k8s.io/apimachinery v0.22.4/go.mod h1:yU6oA6Gnax9RrxGzVvPFFJ+mpnW6PBSqp0sx0I0HHW0=
 k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
 k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
 k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
 k8s.io/apiserver v0.22.1/go.mod h1:2mcM6dzSt+XndzVQJX21Gx0/Klo7Aen7i0Ai6tIa400=
-k8s.io/apiserver v0.22.3/go.mod h1:oam7lH/F1Kto/WTamyQYrD68fS0mGUBORAFf6x/9Mxs=
 k8s.io/apiserver v0.22.4/go.mod h1:38WmcUZiiy41A7Aty8/VorWRa8vDGqoUzDf2XYlku0E=
 k8s.io/cli-runtime v0.22.1 h1:WIueieKvT+IiSVSFosRLI6rkM0tyBGEGH1WUEztVjho=
 k8s.io/cli-runtime v0.22.1/go.mod h1:YqwGrlXeEk15Yn3em2xzr435UGwbrCw5x+COQoTYfoo=
@@ -1546,19 +1546,16 @@ k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
 k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
 k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
 k8s.io/client-go v0.22.1/go.mod h1:BquC5A4UOo4qVDUtoc04/+Nxp1MeHcVc1HJm1KmG8kk=
-k8s.io/client-go v0.22.3/go.mod h1:ElDjYf8gvZsKDYexmsmnMQ0DYO8W9RwBjfQ1PI53yow=
 k8s.io/client-go v0.22.4 h1:aAQ1Wk+I3bjCNk35YWUqbaueqrIonkfDPJSPDDe8Kfg=
 k8s.io/client-go v0.22.4/go.mod h1:Yzw4e5e7h1LNHA4uqnMVrpEpUs1hJOiuBsJKIlRCHDA=
 k8s.io/code-generator v0.18.8/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
 k8s.io/code-generator v0.22.1/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o=
-k8s.io/code-generator v0.22.3/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o=
 k8s.io/code-generator v0.22.4 h1:h7lBa5IuEUC4OQ45q/gIip/a0iQcML2iwrRmXksau30=
 k8s.io/code-generator v0.22.4/go.mod h1:qjYl54pQ/emhkT0UxbufbREYJMWsHNNV/jSVwhYZQGw=
 k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
 k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
 k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
 k8s.io/component-base v0.22.1/go.mod h1:0D+Bl8rrnsPN9v0dyYvkqFfBeAd4u7n77ze+p8CMiPo=
-k8s.io/component-base v0.22.3/go.mod h1:kuybv1miLCMoOk3ebrqF93GbQHQx6W2287FC0YEQY6s=
 k8s.io/component-base v0.22.4/go.mod h1:MrSaQy4a3tFVViff8TZL6JHYSewNCLshZCwHYM58v5A=
 k8s.io/component-helpers v0.22.1/go.mod h1:QvBcDbX+qU5I2tMZABBF5fRwAlQwiv771IGBHK9WYh4=
 k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
diff --git a/jaeger/cmd/check.go b/jaeger/cmd/check.go
index 11f814be2..0445c5608 100644
--- a/jaeger/cmd/check.go
+++ b/jaeger/cmd/check.go
@@ -241,6 +241,7 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
 	hc.AppendCategories(jaegerCategory(hc))
 
 	success := healthcheck.RunChecks(wout, werr, hc, options.output)
+	healthcheck.PrintChecksResult(wout, options.output, success)
 
 	if !success {
 		os.Exit(1)
diff --git a/multicluster/cmd/check.go b/multicluster/cmd/check.go
index c26579c07..22bfe2240 100644
--- a/multicluster/cmd/check.go
+++ b/multicluster/cmd/check.go
@@ -157,6 +157,7 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
 	category := multiclusterCategory(hc)
 	hc.AppendCategories(category)
 	success := healthcheck.RunChecks(wout, werr, hc, options.output)
+	healthcheck.PrintChecksResult(wout, options.output, success)
 	if !success {
 		os.Exit(1)
 	}
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index e54d190ae..f8a5b99d4 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -385,6 +385,7 @@ func (c *Category) WithHintBaseURL(hintBaseURL string) *Category {
 
 // Options specifies configuration for a HealthChecker.
 type Options struct {
+	IsMainCheckCommand    bool
 	ControlPlaneNamespace string
 	CNINamespace          string
 	DataPlaneNamespace    string
diff --git a/pkg/healthcheck/healthcheck_output.go b/pkg/healthcheck/healthcheck_output.go
index 4e5a8712f..dc0e4e4d7 100644
--- a/pkg/healthcheck/healthcheck_output.go
+++ b/pkg/healthcheck/healthcheck_output.go
@@ -61,23 +61,45 @@ func (cr CheckResults) RunChecks(observer CheckObserver) bool {
 	return success
 }
 
-// PrintCoreChecksHeader writes the core checks header.
-func PrintCoreChecksHeader(wout io.Writer) {
-	headerTxt := "Linkerd core checks"
+// PrintChecksHeader writes the core/extension checks header.
+func PrintChecksHeader(wout io.Writer, isCore bool) {
+	var headerTxt string
+	if isCore {
+		headerTxt = "Linkerd core checks"
+	} else {
+		headerTxt = "Linkerd extensions checks"
+		// this ensures there is a new line between the core check status and the extensions header
+		fmt.Fprintln(wout)
+	}
 	fmt.Fprintln(wout, headerTxt)
 	fmt.Fprintln(wout, strings.Repeat("=", len(headerTxt)))
 	fmt.Fprintln(wout)
 }
 
+// PrintChecksResult writes the checks result.
+func PrintChecksResult(wout io.Writer, output string, success bool) {
+	if output == JSONOutput {
+		return
+	}
+
+	switch success {
+	case true:
+		if output != ShortOutput {
+			fmt.Fprintln(wout, "")
+		}
+		fmt.Fprintf(wout, "Status check results are %s\n", okStatus)
+	case false:
+		fmt.Fprintln(wout, "")
+		fmt.Fprintf(wout, "Status check results are %s\n", failStatus)
+	}
+}
+
 // RunExtensionsChecks runs checks for each extension name passed into the `extensions` parameter
 // and handles formatting the output for each extension's check. This function also handles
 // finding the extension in the user's path and runs it.
 func RunExtensionsChecks(wout io.Writer, werr io.Writer, extensions []string, flags []string, output string) bool {
-	if output != JSONOutput {
-		headerTxt := "Linkerd extensions checks"
-		fmt.Fprintln(wout)
-		fmt.Fprintln(wout, headerTxt)
-		fmt.Fprintln(wout, strings.Repeat("=", len(headerTxt)))
+	if output == TableOutput {
+		PrintChecksHeader(wout, false)
 	}
 
 	spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
@@ -146,8 +168,7 @@ func RunExtensionsChecks(wout io.Writer, werr io.Writer, extensions []string, fl
 				results.Results = append(results.Results, extensionResults.Results...)
 			}
 		}
-		// add a new line to space out each check output
-		fmt.Fprintln(wout)
+
 		extensionSuccess := RunChecks(wout, werr, results, output)
 		if !extensionSuccess {
 			success = false
@@ -172,11 +193,9 @@ func runChecksTable(wout io.Writer, hc Runner, output string) bool {
 	spin.Writer = wout
 
 	// We set up different printing functions because we need to handle
-	// 3 check formatting output use cases:
+	// 2 check formatting output use cases:
 	//  1. the default check output in `table` format
-	//  2. the summarized output in `short` format for core checks only
-	//  3. the summarized output in `short` format for extension checks
-	//     and core checks together
+	//  2. the summarized output in `short` format
 	prettyPrintResults := func(result *CheckResult) {
 		lastCategory = printCategory(wout, lastCategory, result)
 
@@ -191,12 +210,14 @@ func runChecksTable(wout io.Writer, hc Runner, output string) bool {
 		printResultDescription(wout, status, result)
 	}
 
+	var headerPrinted bool
 	prettyPrintResultsShort := func(result *CheckResult) {
 		// bail out early and skip printing if we've got an okStatus
 		if result.Err == nil {
 			return
 		}
 
+		headerPrinted = printHeader(wout, headerPrinted, hc)
 		lastCategory = printCategory(wout, lastCategory, result)
 
 		spin.Stop()
@@ -210,46 +231,14 @@ func runChecksTable(wout io.Writer, hc Runner, output string) bool {
 		printResultDescription(wout, status, result)
 	}
 
-	prettyPrintResultsExtensionShort := func(result *CheckResult) {
-		lastCategory = printCategory(wout, lastCategory, result)
-
-		// bail out early and skip printing if we've got an okStatus
-		// Note: we still print the category headers to show which
-		// extension check we are running.
-		if result.Err == nil {
-			return
-		}
-
-		spin.Stop()
-		if result.Retry {
-			restartSpinner(spin, result)
-			return
-		}
-
-		status := getResultStatus(result)
-
-		printResultDescription(wout, status, result)
-	}
-
 	var success bool
 	switch output {
-	case "short":
+	case ShortOutput:
 		success = hc.RunChecks(prettyPrintResultsShort)
-	case "extension-short":
-		success = hc.RunChecks(prettyPrintResultsExtensionShort)
 	default:
 		success = hc.RunChecks(prettyPrintResults)
 	}
 
-	// this empty line separates final results from the checks list in the output
-	fmt.Fprintln(wout, "")
-
-	if !success {
-		fmt.Fprintf(wout, "Status check results are %s\n", failStatus)
-	} else {
-		fmt.Fprintf(wout, "Status check results are %s\n", okStatus)
-	}
-
 	return success
 }
 
@@ -396,6 +385,28 @@ func restartSpinner(spin *spinner.Spinner, result *CheckResult) {
 	}
 }
 
+// When running in short mode, we defer writing the header
+// until the first time we print a warning or error result.
+func printHeader(wout io.Writer, headerPrinted bool, hc Runner) bool {
+	if headerPrinted {
+		return headerPrinted
+	}
+
+	switch v := hc.(type) {
+	case *HealthChecker:
+		if v.IsMainCheckCommand {
+			PrintChecksHeader(wout, true)
+			headerPrinted = true
+		}
+	// When RunExtensionChecks called
+	case CheckResults:
+		PrintChecksHeader(wout, false)
+		headerPrinted = true
+	}
+
+	return headerPrinted
+}
+
 func printCategory(wout io.Writer, lastCategory CategoryID, result *CheckResult) CategoryID {
 	if lastCategory == result.Category {
 		return lastCategory
diff --git a/pkg/k8s/api.go b/pkg/k8s/api.go
index 04d1acf00..3c24d8763 100644
--- a/pkg/k8s/api.go
+++ b/pkg/k8s/api.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 	"time"
 
+	crdclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned"
 	"github.com/linkerd/linkerd2/pkg/prometheus"
 	tsclient "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned"
 	appsv1 "k8s.io/api/apps/v1"
@@ -41,6 +42,7 @@ type KubernetesAPI struct {
 	Apiregistration apiregistration.Interface     // for access to APIService
 	TsClient        tsclient.Interface
 	DynamicClient   dynamic.Interface
+	L5dCrdClient    crdclient.Interface
 }
 
 // NewAPI validates a Kubernetes config and returns a client for accessing the
@@ -92,6 +94,11 @@ func NewAPIForConfig(config *rest.Config, impersonate string, impersonateGroup [
 		return nil, fmt.Errorf("error configuring Kubernetes Dynamic Client: %v", err)
 	}
 
+	l5dCrdClient, err := crdclient.NewForConfig(config)
+	if err != nil {
+		return nil, fmt.Errorf("error configuring Linkerd CRD clientset: %v", err)
+	}
+
 	return &KubernetesAPI{
 		Config:          config,
 		Interface:       clientset,
@@ -99,6 +106,7 @@ func NewAPIForConfig(config *rest.Config, impersonate string, impersonateGroup [
 		Apiregistration: aggregatorClient,
 		TsClient:        tsClient,
 		DynamicClient:   dynamicClient,
+		L5dCrdClient:    l5dCrdClient,
 	}, nil
 }
 
diff --git a/pkg/k8s/authz.go b/pkg/k8s/authz.go
index c01150edb..af441d19f 100644
--- a/pkg/k8s/authz.go
+++ b/pkg/k8s/authz.go
@@ -110,6 +110,24 @@ func ServiceProfilesAccess(ctx context.Context, k8sClient kubernetes.Interface)
 	return errors.New("ServiceProfile CRD not found")
 }
 
+// ServersAccess checks whether the Server CRD is installed on the cluster
+// and the client is authorized to access Servers.
+func ServersAccess(ctx context.Context, k8sClient kubernetes.Interface) error {
+	groupVersion := fmt.Sprintf("%s/%s", PolicyAPIGroup, PolicyAPIVersion)
+	res, err := k8sClient.Discovery().ServerResourcesForGroupVersion(groupVersion)
+	if err != nil {
+		return err
+	}
+	if res.GroupVersion == groupVersion {
+		for _, apiRes := range res.APIResources {
+			if apiRes.Kind == Server {
+				return ResourceAuthz(ctx, k8sClient, "", "list", PolicyAPIGroup, "", "servers", "")
+			}
+		}
+	}
+	return errors.New("Server CRD not found")
+}
+
 // EndpointSliceAccess verifies whether the K8s cluster has
 // access to EndpointSlice resources.
 func EndpointSliceAccess(ctx context.Context, k8sClient kubernetes.Interface) error {
diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go
index ea8f3ce11..ede1f436a 100644
--- a/pkg/k8s/k8s.go
+++ b/pkg/k8s/k8s.go
@@ -28,6 +28,9 @@ const (
 	Server                = "server"
 	ServerAuthorization   = "serverauthorization"
 
+	PolicyAPIGroup   = "policy.linkerd.io"
+	PolicyAPIVersion = "v1beta1"
+
 	ServiceProfileAPIVersion = "linkerd.io/v1alpha2"
 	ServiceProfileKind       = "ServiceProfile"
 
diff --git a/pkg/k8s/policy.go b/pkg/k8s/policy.go
index 06c6a7a71..b2c8a79aa 100644
--- a/pkg/k8s/policy.go
+++ b/pkg/k8s/policy.go
@@ -10,7 +10,6 @@ import (
 	serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/client-go/kubernetes"
 )
 
@@ -42,30 +41,29 @@ func ServerAuthorizationsForResource(ctx context.Context, k8sAPI *KubernetesAPI,
 
 	results := make([]ServerAndAuthorization, 0)
 
-	sazs, err := k8sAPI.DynamicClient.Resource(SazGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+	sazs, err := k8sAPI.L5dCrdClient.ServerauthorizationV1beta1().ServerAuthorizations(namespace).List(ctx, metav1.ListOptions{})
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Failed to get serverauthorization resources: %s\n", err)
 		os.Exit(1)
 	}
 
 	for _, saz := range sazs.Items {
-		var servers []unstructured.Unstructured
+		var servers []serverv1beta1.Server
 
-		if name, found, _ := unstructured.NestedString(saz.UnstructuredContent(), "spec", "server", "name"); found {
-			server, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).Get(ctx, name, metav1.GetOptions{})
+		if saz.Spec.Server.Name != "" {
+			server, err := k8sAPI.L5dCrdClient.ServerV1beta1().Servers(saz.GetNamespace()).Get(ctx, saz.Spec.Server.Name, metav1.GetOptions{})
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "Failed to get server %s: %s\n", name, err)
+				fmt.Fprintf(os.Stderr, "Failed to get server %s: %s\n", saz.Spec.Server.Name, err)
 				os.Exit(1)
 			}
-			servers = []unstructured.Unstructured{*server}
-		} else if sel, found, _ := unstructured.NestedMap(saz.UnstructuredContent(), "spec", "server", "selector"); found {
-			labelSelector := selector(sel)
-			selector, err := metav1.LabelSelectorAsSelector(&labelSelector)
+			servers = []serverv1beta1.Server{*server}
+		} else if saz.Spec.Server.Selector != nil {
+			selector, err := metav1.LabelSelectorAsSelector(saz.Spec.Server.Selector)
 			if err != nil {
 				fmt.Fprintf(os.Stderr, "Failed to get servers: %s\n", err)
 				os.Exit(1)
 			}
-			serverList, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
+			serverList, err := k8sAPI.L5dCrdClient.ServerV1beta1().Servers(saz.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
 			if err != nil {
 				fmt.Fprintf(os.Stderr, "Failed to get servers: %s\n", err)
 				os.Exit(1)
@@ -74,23 +72,23 @@ func ServerAuthorizationsForResource(ctx context.Context, k8sAPI *KubernetesAPI,
 		}
 
 		for _, server := range servers {
-			if sel, found, _ := unstructured.NestedMap(server.UnstructuredContent(), "spec", "podSelector"); found {
-				labelSelector := selector(sel)
-				selector, err := metav1.LabelSelectorAsSelector(&labelSelector)
-				if err != nil {
-					fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
-					os.Exit(1)
-				}
-				selectedPods, err := k8sAPI.CoreV1().Pods(server.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
-				if err != nil {
-					fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
-					os.Exit(1)
-				}
-				if serverIncludesPod(server, selectedPods.Items, podSet) {
-					results = append(results, ServerAndAuthorization{server.GetName(), saz.GetName()})
-				}
+			if server.Spec.PodSelector == nil {
+				continue
 			}
 
+			selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
+				os.Exit(1)
+			}
+			selectedPods, err := k8sAPI.CoreV1().Pods(server.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
+				os.Exit(1)
+			}
+			if serverIncludesPod(server, selectedPods.Items, podSet) {
+				results = append(results, ServerAndAuthorization{server.GetName(), saz.GetName()})
+			}
 		}
 	}
 	return results, nil
@@ -110,30 +108,30 @@ func ServersForResource(ctx context.Context, k8sAPI *KubernetesAPI, namespace st
 
 	results := make([]string, 0)
 
-	servers, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+	servers, err := k8sAPI.L5dCrdClient.ServerV1beta1().Servers(namespace).List(ctx, metav1.ListOptions{})
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Failed to get serverauthorization resources: %s\n", err)
 		os.Exit(1)
 	}
 
 	for _, server := range servers.Items {
-		if sel, found, _ := unstructured.NestedMap(server.UnstructuredContent(), "spec", "podSelector"); found {
-			labelSelector := selector(sel)
-			selector, err := metav1.LabelSelectorAsSelector(&labelSelector)
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
-				os.Exit(1)
-			}
-			selectedPods, err := k8sAPI.CoreV1().Pods(server.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
-				os.Exit(1)
-			}
-			if serverIncludesPod(server, selectedPods.Items, podSet) {
-				results = append(results, server.GetName())
-			}
+		if server.Spec.PodSelector == nil {
+			continue
 		}
 
+		selector, err := metav1.LabelSelectorAsSelector(server.Spec.PodSelector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
+			os.Exit(1)
+		}
+		selectedPods, err := k8sAPI.CoreV1().Pods(server.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
+			os.Exit(1)
+		}
+		if serverIncludesPod(server, selectedPods.Items, podSet) {
+			results = append(results, server.GetName())
+		}
 	}
 	return results, nil
 }
@@ -143,30 +141,29 @@ func ServersForResource(ctx context.Context, k8sAPI *KubernetesAPI, namespace st
 func ServerAuthorizationsForServer(ctx context.Context, k8sAPI *KubernetesAPI, namespace string, server string) ([]string, error) {
 	results := make([]string, 0)
 
-	sazs, err := k8sAPI.DynamicClient.Resource(SazGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+	sazs, err := k8sAPI.L5dCrdClient.ServerauthorizationV1beta1().ServerAuthorizations(namespace).List(ctx, metav1.ListOptions{})
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Failed to get serverauthorization resources: %s\n", err)
 		os.Exit(1)
 	}
 
 	for _, saz := range sazs.Items {
-		if name, found, _ := unstructured.NestedString(saz.UnstructuredContent(), "spec", "server", "name"); found {
-			s, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).Get(ctx, name, metav1.GetOptions{})
+		if saz.Spec.Server.Name != "" {
+			s, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).Get(ctx, saz.Spec.Server.Name, metav1.GetOptions{})
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "Failed to get server %s: %s\n", name, err)
+				fmt.Fprintf(os.Stderr, "Failed to get server %s: %s\n", saz.Spec.Server.Name, err)
 				os.Exit(1)
 			}
 			if s.GetName() == server {
 				results = append(results, saz.GetName())
 			}
-		} else if sel, found, _ := unstructured.NestedMap(saz.UnstructuredContent(), "spec", "server", "selector"); found {
-			labelSelector := selector(sel)
-			selector, err := metav1.LabelSelectorAsSelector(&labelSelector)
+		} else if saz.Spec.Server.Selector != nil {
+			selector, err := metav1.LabelSelectorAsSelector(saz.Spec.Server.Selector)
 			if err != nil {
 				fmt.Fprintf(os.Stderr, "Failed to get servers: %s\n", err)
 				os.Exit(1)
 			}
-			serverList, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
+			serverList, err := k8sAPI.L5dCrdClient.ServerV1beta1().Servers(saz.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
 			if err != nil {
 				fmt.Fprintf(os.Stderr, "Failed to get servers: %s\n", err)
 				os.Exit(1)
@@ -182,58 +179,17 @@ func ServerAuthorizationsForServer(ctx context.Context, k8sAPI *KubernetesAPI, n
 	return results, nil
 }
 
-func selector(selector map[string]interface{}) metav1.LabelSelector {
-	if labels, found, err := unstructured.NestedStringMap(selector, "matchLabels"); found && err == nil {
-		return metav1.LabelSelector{MatchLabels: labels}
-	}
-	if expressions, found, err := unstructured.NestedSlice(selector, "matchExpressions"); found && err == nil {
-		exprs := make([]metav1.LabelSelectorRequirement, len(expressions))
-		for i, expr := range expressions {
-			exprs[i] = matchExpression(expr)
-		}
-		return metav1.LabelSelector{MatchExpressions: exprs}
-	}
-	return metav1.LabelSelector{}
-}
-
-func matchExpression(expr interface{}) metav1.LabelSelectorRequirement {
-	if exprMap, ok := expr.(map[string]interface{}); ok {
-		if key, found, err := unstructured.NestedString(exprMap, "key"); found && err == nil {
-			if op, found, err := unstructured.NestedString(exprMap, "operator"); found && err == nil {
-				if values, found, err := unstructured.NestedStringSlice(exprMap, "values"); found && err == nil {
-					return metav1.LabelSelectorRequirement{
-						Key:      key,
-						Operator: metav1.LabelSelectorOperator(op),
-						Values:   values,
-					}
-				}
-			}
-		}
-	}
-	return metav1.LabelSelectorRequirement{}
-}
-
-func serverIncludesPod(server unstructured.Unstructured, serverPods []corev1.Pod, podSet map[id]struct{}) bool {
+func serverIncludesPod(server serverv1beta1.Server, serverPods []corev1.Pod, podSet map[id]struct{}) bool {
 	for _, pod := range serverPods {
 		if _, ok := podSet[id{pod.Name, pod.Namespace}]; ok {
-			if port, found, err := unstructured.NestedInt64(server.UnstructuredContent(), "spec", "port"); found && err == nil {
-				for _, container := range pod.Spec.Containers {
-					for _, p := range container.Ports {
-						if int32(port) == p.ContainerPort {
-							return true
-						}
-					}
-				}
-			}
-			if port, found, err := unstructured.NestedString(server.UnstructuredContent(), "spec", "port"); found && err == nil {
-				for _, container := range pod.Spec.Containers {
-					for _, p := range container.Ports {
-						if port == p.Name {
-							return true
-						}
+			for _, container := range pod.Spec.Containers {
+				for _, p := range container.Ports {
+					if server.Spec.Port.IntVal == p.ContainerPort || server.Spec.Port.StrVal == p.Name {
+						return true
 					}
 				}
 			}
+
 		}
 	}
 	return false
diff --git a/viz/cmd/check.go b/viz/cmd/check.go
index 6be86a5de..c8d4641e7 100644
--- a/viz/cmd/check.go
+++ b/viz/cmd/check.go
@@ -93,6 +93,7 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
 		hc.AppendCategories(hc.VizDataPlaneCategory())
 	}
 	success := healthcheck.RunChecks(wout, werr, hc, options.output)
+	healthcheck.PrintChecksResult(wout, options.output, success)
 
 	if !success {
 		os.Exit(1)


Test patch:
diff --git a/cli/cmd/testdata/check_output.golden b/cli/cmd/testdata/check_output.golden
index db4ea9a1a..abd51f38a 100644
--- a/cli/cmd/testdata/check_output.golden
+++ b/cli/cmd/testdata/check_output.golden
@@ -4,5 +4,3 @@ category
 × check2
     This should contain instructions for fail
     see https://linkerd.io/2/checks/#hint-anchor for hints
-
-Status check results are ×
diff --git a/controller/k8s/test_helper.go b/controller/k8s/test_helper.go
index ef8adc655..661028dfc 100644
--- a/controller/k8s/test_helper.go
+++ b/controller/k8s/test_helper.go
@@ -33,5 +33,6 @@ func NewFakeAPI(configs ...string) (*API, error) {
 		TS,
 		Node,
 		ES,
+		Srv,
 	), nil
 }
diff --git a/pkg/k8s/policy_test.go b/pkg/k8s/policy_test.go
deleted file mode 100644
index ddc57755e..000000000
--- a/pkg/k8s/policy_test.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package k8s
-
-import (
-	"testing"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-func TestSelector(t *testing.T) {
-	var tests = []struct {
-		name     string
-		selector map[string]interface{}
-		expected string
-	}{
-		{
-			name: "Test `{}` matchLabels",
-			selector: map[string]interface{}{
-				"matchLabels": "{}",
-			},
-			expected: "",
-		},
-		{
-			name: "Test non-empty matchLabels",
-			selector: map[string]interface{}{
-				"matchLabels": map[string]interface{}{
-					"part-of": "viz",
-					"app":     "metrics-api",
-				},
-			},
-			expected: "app=metrics-api,part-of=viz",
-		},
-		{
-			name: "Test `{}` matchExpressions",
-			selector: map[string]interface{}{
-				"matchExpressions": "{}",
-			},
-			expected: "",
-		},
-		{
-			name: "Test non-empty matchExpressions",
-			selector: map[string]interface{}{
-				"matchExpressions": []interface{}{
-					map[string]interface{}{
-						"key":      "app",
-						"operator": "In",
-						"values":   []interface{}{"metrics-api", "prometheus"},
-					},
-					map[string]interface{}{
-						"key":      "part-of",
-						"operator": "NotIn",
-						"values":   []interface{}{"viz-2"},
-					},
-				},
-			},
-			expected: "app in (metrics-api,prometheus),part-of notin (viz-2)",
-		},
-	}
-
-	for _, test := range tests {
-		// pin
-		test := test
-		t.Run(test.name, func(t *testing.T) {
-			labelSelector := selector(test.selector)
-			selector, err := metav1.LabelSelectorAsSelector(&labelSelector)
-			if err != nil {
-				t.Errorf("unexpected error: %v", err)
-			}
-			if selector.String() != test.expected {
-				t.Errorf("expected: %v, got: %v", test.expected, selector.String())
-			}
-		})
-	}
-}
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index 7af1df9d9..6b428a3b0 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -169,7 +169,7 @@ func TestInstallCalico(t *testing.T) {
 		return
 	}
 
-	out, err := TestHelper.Kubectl("", []string{"apply", "-f", "https://k3d.io/usage/guides/calico.yaml"}...)
+	out, err := TestHelper.Kubectl("", []string{"apply", "-f", "https://k3d.io/v5.1.0/usage/advanced/calico.yaml"}...)
 	if err != nil {
 		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
 			"kubectl apply command failed\n%s", out)
diff --git a/test/integration/testdata/check.cni.golden b/test/integration/testdata/check.cni.golden
index a8eab0181..ee1bcd4cd 100644
--- a/test/integration/testdata/check.cni.golden
+++ b/test/integration/testdata/check.cni.golden
@@ -75,5 +75,3 @@ linkerd-control-plane-proxy
 √ control plane proxies are healthy
 √ control plane proxies are up-to-date
 √ control plane proxies and cli versions match
-
-Status check results are √
diff --git a/test/integration/testdata/check.cni.proxy.golden b/test/integration/testdata/check.cni.proxy.golden
index 168fd4a08..6a9b9262a 100644
--- a/test/integration/testdata/check.cni.proxy.golden
+++ b/test/integration/testdata/check.cni.proxy.golden
@@ -84,5 +84,3 @@ linkerd-data-plane
 √ data plane service labels are configured correctly
 √ data plane service annotations are configured correctly
 √ opaque ports are properly annotated
-
-Status check results are √
diff --git a/test/integration/testdata/check.config.golden b/test/integration/testdata/check.config.golden
index c04d6220e..34bbecf6a 100644
--- a/test/integration/testdata/check.config.golden
+++ b/test/integration/testdata/check.config.golden
@@ -25,5 +25,3 @@ linkerd-version
 ---------------
 √ can determine the latest version
 √ cli is up-to-date
-
-Status check results are √
diff --git a/test/integration/testdata/check.golden b/test/integration/testdata/check.golden
index c9a2411c7..16f98ba3e 100644
--- a/test/integration/testdata/check.golden
+++ b/test/integration/testdata/check.golden
@@ -66,5 +66,3 @@ linkerd-control-plane-proxy
 √ control plane proxies are healthy
 √ control plane proxies are up-to-date
 √ control plane proxies and cli versions match
-
-Status check results are √
diff --git a/test/integration/testdata/check.proxy.golden b/test/integration/testdata/check.proxy.golden
index b4d70e877..55fe43469 100644
--- a/test/integration/testdata/check.proxy.golden
+++ b/test/integration/testdata/check.proxy.golden
@@ -75,5 +75,3 @@ linkerd-data-plane
 √ data plane service labels are configured correctly
 √ data plane service annotations are configured correctly
 √ opaque ports are properly annotated
-
-Status check results are √