Repository: linkerd/linkerd2
Pull request #6576 (2021-07-29 17:03:45)

Bump webpack from 5.46.0 to 5.47.1 in /web/app
Bumps [webpack](https://github.com/webpack/webpack) from 5.46.0 to 5.47.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/webpack/webpack/releases">webpack's releases</a>.</em></p>
<blockquote>
<h2>v5.47.1</h2>
<h1>Bugfixes</h1>
<ul>
<li>upgrade webpack-sources for a bunch of bugfixes regarding source maps and missing chars in output</li>
</ul>
<h2>v5.47.0</h2>
<h1>Performance</h1>
<ul>
<li>improve source-map performance</li>
</ul>
<h1>Bugfixes</h1>
<ul>
<li>avoid unnecessary <code>&quot;use strict&quot;</code>s in module mode</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/webpack/webpack/commit/3f353b6ae29ae6d96e0ab86e7233b4c785f99c60"><code>3f353b6</code></a> 5.47.1</li>
<li><a href="https://github.com/webpack/webpack/commit/6b6f4fead714a9c301c30417b49c58aba153aae6"><code>6b6f4fe</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/webpack/webpack/issues/13888">#13888</a> from webpack/bugfix/webpack-sources</li>
<li><a href="https://github.com/webpack/webpack/commit/5fc7008c3e2ade90dc7561a51eceb484ae305453"><code>5fc7008</code></a> update webpack-sources to 3.1.1</li>
<li><a href="https://github.com/webpack/webpack/commit/efeb1287e52870036cc01b22476852205e99c31a"><code>efeb128</code></a> 5.47.0</li>
<li><a href="https://github.com/webpack/webpack/commit/61ad5ff509a7f9ce5be703e386e4d4da6216f008"><code>61ad5ff</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/webpack/webpack/issues/13873">#13873</a> from webpack/perf/webpack-sources-3</li>
<li><a href="https://github.com/webpack/webpack/commit/c3a0145b49e62fbda7fe1372b87b0cef517a6c7f"><code>c3a0145</code></a> update to webpack-sources version 3</li>
<li><a href="https://github.com/webpack/webpack/commit/beb42c64f696584ef570d8f57df448ddd0ac7238"><code>beb42c6</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/webpack/webpack/issues/13858">#13858</a> from Janpot/genrator-publicPath-override</li>
<li><a href="https://github.com/webpack/webpack/commit/d9c09ee7f20f174588f1877da65dc57ec1e2ac5a"><code>d9c09ee</code></a> Fix generator publicPath behavior when empty string</li>
<li><a href="https://github.com/webpack/webpack/commit/bdf20c2a6f531022a91960d5f3facf324577d1ec"><code>bdf20c2</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/webpack/webpack/issues/13840">#13840</a> from snitin315/patch-1</li>
<li><a href="https://github.com/webpack/webpack/commit/5f8adb0110f824c433c2619b072cdd467875434a"><code>5f8adb0</code></a> keep the eslint cache and yarn install</li>
<li>Additional commits viewable in <a href="https://github.com/webpack/webpack/compare/v5.46.0...v5.47.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack&package-manager=npm_and_yarn&previous-version=5.46.0&new-version=5.47.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>

codecov-commenter: # [Codecov](https://codecov.io/gh/linkerd/linkerd2/pull/6576?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) Report
> Merging [#6576](https://codecov.io/gh/linkerd/linkerd2/pull/6576?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) (4557e3c) into [main](https://codecov.io/gh/linkerd/linkerd2/commit/ca1077bb08c5790317345377a92c3303034b4d40?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) (ca1077b) will **increase** coverage by `21.89%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/linkerd/linkerd2/pull/6576/graphs/tree.svg?width=650&height=150&src=pr&token=FCBISvpqTv&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd)](https://codecov.io/gh/linkerd/linkerd2/pull/6576?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd)

```diff
@@             Coverage Diff             @@
##             main    #6576       +/-   ##
===========================================
+ Coverage   44.86%   66.76%   +21.89%     
===========================================
  Files         182       39      -143     
  Lines       23866     1396    -22470     
  Branches      290      290               
===========================================
- Hits        10707      932     -9775     
+ Misses      12407      460    -11947     
+ Partials      752        4      -748     
```

| Flag | Coverage Δ | |
|---|---|---|
| golang | `?` | |
| javascript | `66.76% <ø> (ø)` | |
| unittests | `66.76% <ø> (+21.89%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/linkerd/linkerd2/pull/6576?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) | Coverage Δ | |
|---|---|---|
| [...luster/service-mirror/cluster\_watcher\_test\_util.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-bXVsdGljbHVzdGVyL3NlcnZpY2UtbWlycm9yL2NsdXN0ZXJfd2F0Y2hlcl90ZXN0X3V0aWwuZ28=) | | |
| [controller/k8s/clientset.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-Y29udHJvbGxlci9rOHMvY2xpZW50c2V0Lmdv) | | |
| [pkg/k8s/completion.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-cGtnL2s4cy9jb21wbGV0aW9uLmdv) | | |
| [viz/tap/api/client.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-dml6L3RhcC9hcGkvY2xpZW50Lmdv) | | |
| [jaeger/cmd/dashboard.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-amFlZ2VyL2NtZC9kYXNoYm9hcmQuZ28=) | | |
| [pkg/util/http.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-cGtnL3V0aWwvaHR0cC5nbw==) | | |
| [pkg/k8s/authz.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-cGtnL2s4cy9hdXRoei5nbw==) | | |
| [pkg/profiles/profiles.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-cGtnL3Byb2ZpbGVzL3Byb2ZpbGVzLmdv) | | |
| [controller/heartbeat/heartbeat.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-Y29udHJvbGxlci9oZWFydGJlYXQvaGVhcnRiZWF0Lmdv) | | |
| [cli/cmd/version.go](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd#diff-Y2xpL2NtZC92ZXJzaW9uLmdv) | | |
| ... and [133 more](https://codecov.io/gh/linkerd/linkerd2/pull/6576/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/linkerd/linkerd2/pull/6576?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/linkerd/linkerd2/pull/6576?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd). Last update [ca1077b...4557e3c](https://codecov.io/gh/linkerd/linkerd2/pull/6576?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=linkerd).


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4fb5393d1..f848f5aeb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -239,7 +239,7 @@ jobs:
         mv choco/linkerd.*.nupkg target/release/linkerd2-cli-stable-$VERSION.nupkg || true
     - name: Create release
       id: create_release
-      uses: softprops/action-gh-release@08e53e60c840b6d34ed49473f9ab939eb801dbc4
+      uses: softprops/action-gh-release@fb0163a75bee697a9cfec2c931801de7c7f10042
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       with:
diff --git a/CHANGES.md b/CHANGES.md
index a0a260e02..ada6b6be5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,18 @@
 # Changes
 
+## edge-21.7.5
+
+This release updates Linkerd to store the identity trust root in a ConfigMap to
+make it easier to manage and rotate the trust root.  The release also lays the
+groundwork for StatefulSet support in the multicluster extension and removes
+deprecated PSP resources by default.
+
+* Added a `linkerd-identity-trust-roots` ConfigMap which contains the configured
+  trust root bundle
+* Introduced support for StatefulSets across multicluster (disabled by default)
+* Stopped installing PSP resources by default since these are deprecated as
+  of Kubernetes v1.21
+
 ## edge-21.7.4
 
 This release continues to focus on dependency updates. It also adds the
diff --git a/charts/linkerd2-cni/README.md b/charts/linkerd2-cni/README.md
index 5b4acadbb..7328cdc3b 100644
--- a/charts/linkerd2-cni/README.md
+++ b/charts/linkerd2-cni/README.md
@@ -26,6 +26,7 @@ Kubernetes: `>=1.16.0-0`
 | cniPluginVersion | string | `"linkerdVersionValue"` | Tag for the CNI container Docker image |
 | destCNIBinDir | string | `"/opt/cni/bin"` | Directory on the host where the CNI configuration will be placed |
 | destCNINetDir | string | `"/etc/cni/net.d"` | Directory on the host where the CNI plugin binaries reside |
+| enablePSP | bool | `false` | Add a PSP resource and bind it to the linkerd-cni ServiceAccounts. Note PSP has been deprecated since k8s v1.21 |
 | extraInitContainers | list | `[]` | Add additional initContainers to the daemonset |
 | ignoreInboundPorts | string | `""` | Default set of inbound ports to skip via iptables |
 | ignoreOutboundPorts | string | `""` | Default set of outbound ports to skip via iptables |
diff --git a/charts/linkerd2-cni/templates/cni-plugin.yaml b/charts/linkerd2-cni/templates/cni-plugin.yaml
index 8042b0f64..8c01cdef9 100644
--- a/charts/linkerd2-cni/templates/cni-plugin.yaml
+++ b/charts/linkerd2-cni/templates/cni-plugin.yaml
@@ -29,6 +29,19 @@ metadata:
     config.linkerd.io/admission-webhooks: disabled
 ---
 {{ end -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: linkerd-cni
+  namespace: {{.Values.namespace}}
+  labels:
+    linkerd.io/cni-resource: "true"
+{{- if .Values.imagePullSecrets }}
+imagePullSecrets:
+{{ toYaml .Values.imagePullSecrets | indent 2 }}
+{{- end }}
+{{ if .Values.enablePSP -}}
+---
 apiVersion: policy/v1beta1
 kind: PodSecurityPolicy
 metadata:
@@ -51,18 +64,6 @@ spec:
   - secret
   - emptyDir
 ---
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: linkerd-cni
-  namespace: {{.Values.namespace}}
-  labels:
-    linkerd.io/cni-resource: "true"
-{{- if .Values.imagePullSecrets }}
-imagePullSecrets:
-{{ toYaml .Values.imagePullSecrets | indent 2 }}
-{{- end }}
----
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
@@ -92,6 +93,7 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-cni
   namespace: {{.Values.namespace}}
+{{ end -}}
 ---
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/charts/linkerd2-cni/values.yaml b/charts/linkerd2-cni/values.yaml
index 7e4e9a3db..88dfc09d1 100644
--- a/charts/linkerd2-cni/values.yaml
+++ b/charts/linkerd2-cni/values.yaml
@@ -33,6 +33,10 @@ useWaitFlag:      false
 # -- Kubernetes priorityClassName for the CNI plugin's Pods
 priorityClassName: ""
 
+# -- Add a PSP resource and bind it to the linkerd-cni ServiceAccounts.
+# Note PSP has been deprecated since k8s v1.21
+enablePSP: false
+
 # -|- Tolerations section, See the
 # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
 # for more information
diff --git a/charts/linkerd2/README.md b/charts/linkerd2/README.md
index da62d27f9..d5a5c9dc7 100644
--- a/charts/linkerd2/README.md
+++ b/charts/linkerd2/README.md
@@ -142,6 +142,7 @@ Kubernetes: `>=1.16.0-0`
 | disableHeartBeat | bool | `false` | Set to true to not start the heartbeat cronjob |
 | enableEndpointSlices | bool | `false` | enables the use of EndpointSlice informers for the destination service; enableEndpointSlices should be set to true only if EndpointSlice K8s feature gate is on; the feature is still experimental. |
 | enableH2Upgrade | bool | `true` | Allow proxies to perform transparent HTTP/2 upgrading |
+| enablePSP | bool | `false` | Add a PSP resource and bind it to the control plane ServiceAccounts. Note PSP has been deprecated since k8s v1.21 |
 | identity.externalCA | bool | `false` | If the linkerd-identity-trust-roots ConfigMap has already been created |
 | identity.issuer.clockSkewAllowance | string | `"20s"` | Amount of time to allow for clock skew within a Linkerd cluster |
 | identity.issuer.crtExpiry | string | `nil` | Expiration timestamp for the issuer certificate. It must be provided during install. Must match the expiry date in crtPEM |
diff --git a/charts/linkerd2/templates/psp.yaml b/charts/linkerd2/templates/psp.yaml
index e98f6d5e7..ee4fed4ca 100644
--- a/charts/linkerd2/templates/psp.yaml
+++ b/charts/linkerd2/templates/psp.yaml
@@ -1,3 +1,4 @@
+{{ if .Values.enablePSP -}}
 ---
 ###
 ### Control Plane PSP
@@ -100,3 +101,4 @@ subjects:
 - kind: ServiceAccount
   name: linkerd-sp-validator
   namespace: {{.Values.namespace}}
+{{ end -}}
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index 19f1334d0..8ebb67107 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -151,6 +151,10 @@ imagePullSecrets: []
 # -- Allow proxies to perform transparent HTTP/2 upgrading
 enableH2Upgrade: true
 
+# -- Add a PSP resource and bind it to the control plane ServiceAccounts. Note
+# PSP has been deprecated since k8s v1.21
+enablePSP: false
+
 # -- Omit the `sideEffects` flag in the webhook manifests
 omitWebhookSideEffects: false
 # -- Failure policy for the proxy injector
diff --git a/cli/cmd/check.go b/cli/cmd/check.go
index dfb8295af..cd787e63e 100644
--- a/cli/cmd/check.go
+++ b/cli/cmd/check.go
@@ -167,8 +167,6 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, s
 		checks = append(checks, healthcheck.LinkerdPreInstallChecks)
 		if options.cniEnabled {
 			checks = append(checks, healthcheck.LinkerdCNIPluginChecks)
-		} else {
-			checks = append(checks, healthcheck.LinkerdPreInstallCapabilityChecks)
 		}
 		installManifest, err = renderInstallManifest(cmd.Context())
 		if err != nil {
diff --git a/controller/webhook/launcher.go b/controller/webhook/launcher.go
index e242aa768..97efb001b 100644
--- a/controller/webhook/launcher.go
+++ b/controller/webhook/launcher.go
@@ -37,9 +37,9 @@ func Launch(
 		log.Fatalf("failed to initialize the webhook server: %s", err)
 	}
 
-	k8sAPI.Sync(nil)
-
 	go s.Start()
+
+	k8sAPI.Sync(nil)
 	go admin.StartServer(metricsAddr)
 
 	<-stop
diff --git a/jaeger/charts/linkerd-jaeger/README.md b/jaeger/charts/linkerd-jaeger/README.md
index b85ea33af..4cb3db335 100644
--- a/jaeger/charts/linkerd-jaeger/README.md
+++ b/jaeger/charts/linkerd-jaeger/README.md
@@ -86,6 +86,7 @@ Kubernetes: `>=1.16.0-0`
 | collector.image.version | string | `"0.27.0"` |  |
 | collector.nodeSelector | object | `{"beta.kubernetes.io/os":"linux"}` | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information |
 | collector.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
+| enablePSP | bool | `false` | Create Roles and RoleBindings to associate this extension's ServiceAccounts to the control plane PSP resource. This requires that `enabledPSP` is set to true on the control plane install. Note PSP has been deprecated since k8s v1.21 |
 | installNamespace | bool | `true` | Set to false when installing in a custom namespace. |
 | jaeger.enabled | bool | `true` | Set to false to exclude all-in-one Jaeger installation |
 | jaeger.image.name | string | `"jaegertracing/all-in-one"` |  |
diff --git a/jaeger/charts/linkerd-jaeger/templates/psp.yaml b/jaeger/charts/linkerd-jaeger/templates/psp.yaml
index 6f375c8a2..908a386f4 100644
--- a/jaeger/charts/linkerd-jaeger/templates/psp.yaml
+++ b/jaeger/charts/linkerd-jaeger/templates/psp.yaml
@@ -1,3 +1,4 @@
+{{ if .Values.enablePSP -}}
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -38,3 +39,4 @@ subjects:
   name: jaeger
   namespace: {{.Values.namespace}}
 {{ end -}}
+{{ end -}}
diff --git a/jaeger/charts/linkerd-jaeger/values.yaml b/jaeger/charts/linkerd-jaeger/values.yaml
index 1375a43ff..735377d45 100644
--- a/jaeger/charts/linkerd-jaeger/values.yaml
+++ b/jaeger/charts/linkerd-jaeger/values.yaml
@@ -17,6 +17,12 @@ nodeSelector: &default_node_selector
 # for more information
 tolerations: &default_tolerations
 
+# -- Create Roles and RoleBindings to associate this extension's
+# ServiceAccounts to the control plane PSP resource. This requires that
+# `enabledPSP` is set to true on the control plane install. Note PSP has been
+# deprecated since k8s v1.21
+enablePSP: false
+
 collector:
   # -- Set to false to exclude collector installation
   enabled: true
diff --git a/multicluster/charts/linkerd-multicluster/README.md b/multicluster/charts/linkerd-multicluster/README.md
index 7593e34fc..2143a449e 100644
--- a/multicluster/charts/linkerd-multicluster/README.md
+++ b/multicluster/charts/linkerd-multicluster/README.md
@@ -79,6 +79,7 @@ Kubernetes: `>=1.16.0-0`
 
 | Key | Type | Default | Description |
 |-----|------|---------|-------------|
+| enablePSP | bool | `false` | Create Roles and RoleBindings to associate this extension's ServiceAccounts to the control plane PSP resource. This requires that `enabledPSP` is set to true on the control plane install. Note PSP has been deprecated since k8s v1.21 |
 | gateway.enabled | bool | `true` | If the gateway component should be installed |
 | gateway.loadBalancerIP | string | `""` | Set loadBalancerIP on gateway service |
 | gateway.name | string | `"linkerd-gateway"` | The name of the gateway that will be installed |
diff --git a/multicluster/charts/linkerd-multicluster/templates/psp.yaml b/multicluster/charts/linkerd-multicluster/templates/psp.yaml
index 3725dd86e..c8bf3c5b7 100644
--- a/multicluster/charts/linkerd-multicluster/templates/psp.yaml
+++ b/multicluster/charts/linkerd-multicluster/templates/psp.yaml
@@ -1,3 +1,4 @@
+{{ if .Values.enablePSP -}}
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -29,3 +30,4 @@ subjects:
 - kind: ServiceAccount
   name: {{.Values.gateway.name}}
   namespace: {{.Values.namespace}}
+{{ end -}}
diff --git a/multicluster/charts/linkerd-multicluster/templates/remote-access-service-mirror-rbac.yaml b/multicluster/charts/linkerd-multicluster/templates/remote-access-service-mirror-rbac.yaml
index bd34e08ea..39038c8ee 100644
--- a/multicluster/charts/linkerd-multicluster/templates/remote-access-service-mirror-rbac.yaml
+++ b/multicluster/charts/linkerd-multicluster/templates/remote-access-service-mirror-rbac.yaml
@@ -16,12 +16,15 @@ metadata:
     {{ include "partials.annotations.created-by" $ }}
 rules:
 - apiGroups: [""]
-  resources: ["services"]
+  resources: ["services", "endpoints"]
   verbs: ["list", "get", "watch"]
 - apiGroups: [""]
   resources: ["configmaps"]
   verbs: ["get"]
-  resourceNames: ["linkerd-config"]  
+  resourceNames: ["linkerd-config"]
+- apiGroups: [""]
+  resources: ["events"]
+  verbs: ["create", "patch"]
 ---
 apiVersion: v1
 kind: ServiceAccount
diff --git a/multicluster/charts/linkerd-multicluster/values.yaml b/multicluster/charts/linkerd-multicluster/values.yaml
index 2a8835853..3d0b4a04d 100644
--- a/multicluster/charts/linkerd-multicluster/values.yaml
+++ b/multicluster/charts/linkerd-multicluster/values.yaml
@@ -41,3 +41,9 @@ remoteMirrorServiceAccountName: linkerd-service-mirror-remote-access-default
 linkerdNamespace: linkerd
 # -- Identity Trust Domain of the certificate authority
 identityTrustDomain: cluster.local
+
+# -- Create Roles and RoleBindings to associate this extension's
+# ServiceAccounts to the control plane PSP resource. This requires that
+# `enabledPSP` is set to true on the control plane install. Note PSP has been
+# deprecated since k8s v1.21
+enablePSP: false
diff --git a/multicluster/cmd/service-mirror/main.go b/multicluster/cmd/service-mirror/main.go
index d5918e0e4..3c2e62834 100644
--- a/multicluster/cmd/service-mirror/main.go
+++ b/multicluster/cmd/service-mirror/main.go
@@ -39,6 +39,7 @@ func Main(args []string) {
 	metricsAddr := cmd.String("metrics-addr", ":9999", "address to serve scrapable metrics on")
 	namespace := cmd.String("namespace", "", "namespace containing Link and credentials Secret")
 	repairPeriod := cmd.Duration("endpoint-refresh-period", 1*time.Minute, "frequency to refresh endpoint resolution")
+	enableHeadlessSvc := cmd.Bool("enable-headless-services", false, "toggle support for headless service mirroring")
 
 	flags.ConfigureAndParse(cmd, args)
 	linkName := cmd.Arg(0)
@@ -113,7 +114,7 @@ main:
 							if err != nil {
 								log.Errorf("Failed to load remote cluster credentials: %s", err)
 							}
-							err = restartClusterWatcher(ctx, link, *namespace, creds, controllerK8sAPI, *requeueLimit, *repairPeriod, metrics)
+							err = restartClusterWatcher(ctx, link, *namespace, creds, controllerK8sAPI, *requeueLimit, *repairPeriod, metrics, *enableHeadlessSvc)
 							if err != nil {
 								// failed to restart cluster watcher; give a bit of slack
 								// and restart the link watch to give it another try
@@ -162,6 +163,7 @@ func restartClusterWatcher(
 	requeueLimit int,
 	repairPeriod time.Duration,
 	metrics servicemirror.ProbeMetricVecs,
+	enableHeadlessSvc bool,
 ) error {
 	if clusterWatcher != nil {
 		clusterWatcher.Stop(false)
@@ -183,6 +185,7 @@ func restartClusterWatcher(
 		&link,
 		requeueLimit,
 		repairPeriod,
+		enableHeadlessSvc,
 	)
 	if err != nil {
 		return fmt.Errorf("Unable to create cluster watcher: %s", err)
diff --git a/multicluster/service-mirror/cluster_watcher.go b/multicluster/service-mirror/cluster_watcher.go
index f1971a660..3efb33fa9 100644
--- a/multicluster/service-mirror/cluster_watcher.go
+++ b/multicluster/service-mirror/cluster_watcher.go
@@ -13,14 +13,20 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/kubernetes/scheme"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/workqueue"
 )
 
+const eventTypeSkipped = "ServiceMirroringSkipped"
+
 type (
 	// RemoteClusterServiceWatcher is a watcher instantiated for every cluster that is being watched
 	// Its main job is to listen to events coming from the remote cluster and react accordingly, keeping
@@ -30,15 +36,17 @@ type (
 	// it can be requeued up to N times, to ensure that the failure is not due to some temporary network
 	// problems or general glitch in the Matrix.
 	RemoteClusterServiceWatcher struct {
-		serviceMirrorNamespace string
-		link                   *multicluster.Link
-		remoteAPIClient        *k8s.API
-		localAPIClient         *k8s.API
-		stopper                chan struct{}
-		log                    *logging.Entry
-		eventsQueue            workqueue.RateLimitingInterface
-		requeueLimit           int
-		repairPeriod           time.Duration
+		serviceMirrorNamespace  string
+		link                    *multicluster.Link
+		remoteAPIClient         *k8s.API
+		localAPIClient          *k8s.API
+		stopper                 chan struct{}
+		recorder                record.EventRecorder
+		log                     *logging.Entry
+		eventsQueue             workqueue.RateLimitingInterface
+		requeueLimit            int
+		repairPeriod            time.Duration
+		headlessServicesEnabled bool
 	}
 
 	// RemoteServiceCreated is generated whenever a remote service is created Observing
@@ -89,12 +97,23 @@ type (
 		svc *corev1.Service
 	}
 
+	// OnAddEndpointsCalled is issued when the onAdd function of the Endpoints
+	// shared informer is called
+	OnAddEndpointsCalled struct {
+		ep *corev1.Endpoints
+	}
+
 	// OnUpdateCalled is issued when the onUpdate function of the
 	// shared informer is called
 	OnUpdateCalled struct {
 		svc *corev1.Service
 	}
 
+	// OnUpdateEndpointsCalled is issued when the onUpdate function of the
+	// shared Endpoints informer is called
+	OnUpdateEndpointsCalled struct {
+		ep *corev1.Endpoints
+	}
 	// OnDeleteCalled is issued when the onDelete function of the
 	// shared informer is called
 	OnDeleteCalled struct {
@@ -126,9 +145,9 @@ func NewRemoteClusterServiceWatcher(
 	link *multicluster.Link,
 	requeueLimit int,
 	repairPeriod time.Duration,
-
+	enableHeadlessSvc bool,
 ) (*RemoteClusterServiceWatcher, error) {
-	remoteAPI, err := k8s.InitializeAPIForConfig(ctx, cfg, false, k8s.Svc)
+	remoteAPI, err := k8s.InitializeAPIForConfig(ctx, cfg, false, k8s.Svc, k8s.Endpoint)
 	if err != nil {
 		return nil, fmt.Errorf("cannot initialize api for target cluster %s: %s", clusterName, err)
 	}
@@ -137,6 +156,15 @@ func NewRemoteClusterServiceWatcher(
 		return nil, fmt.Errorf("cannot connect to api for target cluster %s: %s", clusterName, err)
 	}
 
+	// Create k8s event recorder
+	eventBroadcaster := record.NewBroadcaster()
+	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{
+		Interface: remoteAPI.Client.CoreV1().Events(""),
+	})
+	recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{
+		Component: fmt.Sprintf("linkerd-service-mirror-%s", clusterName),
+	})
+
 	stopper := make(chan struct{})
 	return &RemoteClusterServiceWatcher{
 		serviceMirrorNamespace: serviceMirrorNamespace,
@@ -144,13 +172,15 @@ func NewRemoteClusterServiceWatcher(
 		remoteAPIClient:        remoteAPI,
 		localAPIClient:         localAPI,
 		stopper:                stopper,
+		recorder:               recorder,
 		log: logging.WithFields(logging.Fields{
 			"cluster":    clusterName,
 			"apiAddress": cfg.Host,
 		}),
-		eventsQueue:  workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
-		requeueLimit: requeueLimit,
-		repairPeriod: repairPeriod,
+		eventsQueue:             workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
+		requeueLimit:            requeueLimit,
+		repairPeriod:            repairPeriod,
+		headlessServicesEnabled: enableHeadlessSvc,
 	}, nil
 }
 
@@ -174,10 +204,12 @@ func (rcsw *RemoteClusterServiceWatcher) getMirroredServiceAnnotations(remoteSer
 		consts.RemoteResourceVersionAnnotation: remoteService.ResourceVersion, // needed to detect real changes
 		consts.RemoteServiceFqName:             fmt.Sprintf("%s.%s.svc.%s", remoteService.Name, remoteService.Namespace, rcsw.link.TargetClusterDomain),
 	}
+
 	value, ok := remoteService.GetAnnotations()[consts.ProxyOpaquePortsAnnotation]
 	if ok {
 		annotations[consts.ProxyOpaquePortsAnnotation] = value
 	}
+
 	return annotations
 }
 
@@ -289,7 +321,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Co
 			if kerrors.IsNotFound(err) {
 				continue
 			}
-			errors = append(errors, fmt.Errorf("Could not delete  service %s/%s: %s", svc.Namespace, svc.Name, err))
+			errors = append(errors, fmt.Errorf("Could not delete service %s/%s: %s", svc.Namespace, svc.Name, err))
 		} else {
 			rcsw.log.Infof("Deleted service %s/%s", svc.Namespace, svc.Name)
 		}
@@ -297,7 +329,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Co
 
 	endpoints, err := rcsw.localAPIClient.Endpoint().Lister().List(labels.Set(matchLabels).AsSelector())
 	if err != nil {
-		innerErr := fmt.Errorf("could not retrieve Endpoints that need cleaning up: %s", err)
+		innerErr := fmt.Errorf("could not retrieve endpoints that need cleaning up: %s", err)
 		if kerrors.IsNotFound(err) {
 			return innerErr
 		}
@@ -309,9 +341,9 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Co
 			if kerrors.IsNotFound(err) {
 				continue
 			}
-			errors = append(errors, fmt.Errorf("Could not delete  Endpoints %s/%s: %s", endpoint.Namespace, endpoint.Name, err))
+			errors = append(errors, fmt.Errorf("Could not delete endpoints %s/%s: %s", endpoint.Namespace, endpoint.Name, err))
 		} else {
-			rcsw.log.Infof("Deleted Endpoints %s/%s", endpoint.Namespace, endpoint.Name)
+			rcsw.log.Infof("Deleted endpoints %s/%s", endpoint.Namespace, endpoint.Name)
 		}
 	}
 
@@ -324,11 +356,43 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Co
 // Deletes a locally mirrored service as it is not present on the remote cluster anymore
 func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ctx context.Context, ev *RemoteServiceDeleted) error {
 	localServiceName := rcsw.mirroredResourceName(ev.Name)
-	rcsw.log.Infof("Deleting mirrored service %s/%s", ev.Namespace, localServiceName)
+	localService, err := rcsw.localAPIClient.Svc().Lister().Services(ev.Namespace).Get(localServiceName)
 	var errors []error
+	if err != nil {
+		if kerrors.IsNotFound(err) {
+			rcsw.log.Debugf("Failed to delete mirror service %s/%s: %v", ev.Namespace, ev.Name, err)
+			return nil
+		}
+		errors = append(errors, fmt.Errorf("could not fetch service %s/%s: %s", ev.Namespace, localServiceName, err))
+	}
+
+	// If the mirror service is headless, also delete its endpoint mirror
+	// services.
+	if localService.Spec.ClusterIP == corev1.ClusterIPNone {
+		matchLabels := map[string]string{
+			consts.MirroredHeadlessSvcNameLabel: localServiceName,
+		}
+		endpointMirrorServices, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
+		if err != nil {
+			if !kerrors.IsNotFound(err) {
+				errors = append(errors, fmt.Errorf("could not fetch endpoint mirrors for mirror service %s/%s: %s", ev.Namespace, localServiceName, err))
+			}
+		}
+
+		for _, endpointMirror := range endpointMirrorServices {
+			err = rcsw.localAPIClient.Client.CoreV1().Services(endpointMirror.Namespace).Delete(ctx, endpointMirror.Name, metav1.DeleteOptions{})
+			if err != nil {
+				if !kerrors.IsNotFound(err) {
+					errors = append(errors, fmt.Errorf("could not delete endpoint mirror %s/%s: %s", endpointMirror.Namespace, endpointMirror.Name, err))
+				}
+			}
+		}
+	}
+
+	rcsw.log.Infof("Deleting mirrored service %s/%s", ev.Namespace, localServiceName)
 	if err := rcsw.localAPIClient.Client.CoreV1().Services(ev.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{}); err != nil {
 		if !kerrors.IsNotFound(err) {
-			errors = append(errors, fmt.Errorf("could not delete Service: %s/%s: %s", ev.Namespace, localServiceName, err))
+			errors = append(errors, fmt.Errorf("could not delete service: %s/%s: %s", ev.Namespace, localServiceName, err))
 		}
 	}
 
@@ -336,7 +400,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ctx context.
 		return RetryableError{errors}
 	}
 
-	rcsw.log.Infof("Successfully deleted Service: %s/%s", ev.Namespace, localServiceName)
+	rcsw.log.Infof("Successfully deleted service: %s/%s", ev.Namespace, localServiceName)
 	return nil
 }
 
@@ -392,12 +456,11 @@ func remapRemoteServicePorts(ports []corev1.ServicePort) []corev1.ServicePort {
 }
 
 func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ctx context.Context, ev *RemoteServiceCreated) error {
-	gatewayAddresses, err := rcsw.resolveGatewayAddress()
-	if err != nil {
-		return err
+	remoteService := ev.service.DeepCopy()
+	if rcsw.headlessServicesEnabled && remoteService.Spec.ClusterIP == corev1.ClusterIPNone {
+		return nil
 	}
 
-	remoteService := ev.service.DeepCopy()
 	serviceInfo := fmt.Sprintf("%s/%s", remoteService.Namespace, remoteService.Name)
 	localServiceName := rcsw.mirroredResourceName(remoteService.Name)
 
@@ -417,49 +480,60 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ctx context.
 		},
 	}
 
+	rcsw.log.Infof("Creating a new service mirror for %s", serviceInfo)
+	if _, err := rcsw.localAPIClient.Client.CoreV1().Services(remoteService.Namespace).Create(ctx, serviceToCreate, metav1.CreateOptions{}); err != nil {
+		if !kerrors.IsAlreadyExists(err) {
+			// we might have created it during earlier attempt, if that is not the case, we retry
+			return RetryableError{[]error{err}}
+		}
+	}
+
+	return rcsw.createGatewayEndpoints(ctx, remoteService)
+}
+
+func (rcsw *RemoteClusterServiceWatcher) createGatewayEndpoints(ctx context.Context, exportedService *corev1.Service) error {
+	gatewayAddresses, err := rcsw.resolveGatewayAddress()
+	if err != nil {
+		return err
+	}
+
+	localServiceName := rcsw.mirroredResourceName(exportedService.Name)
+	serviceInfo := fmt.Sprintf("%s/%s", exportedService.Namespace, exportedService.Name)
 	endpointsToCreate := &corev1.Endpoints{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      localServiceName,
-			Namespace: ev.service.Namespace,
+			Namespace: exportedService.Namespace,
 			Labels: map[string]string{
 				consts.MirroredResourceLabel:  "true",
 				consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName,
 			},
 			Annotations: map[string]string{
-				consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", remoteService.Name, remoteService.Namespace, rcsw.link.TargetClusterDomain),
+				consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", exportedService.Name, exportedService.Namespace, rcsw.link.TargetClusterDomain),
 			},
 		},
 	}
 
-	// only if we resolve it, we are updating the endpoints addresses and ports
 	rcsw.log.Infof("Resolved gateway [%v:%d] for %s", gatewayAddresses, rcsw.link.GatewayPort, serviceInfo)
 
 	if len(gatewayAddresses) > 0 {
 		endpointsToCreate.Subsets = []corev1.EndpointSubset{
 			{
 				Addresses: gatewayAddresses,
-				Ports:     rcsw.getEndpointsPorts(ev.service),
+				Ports:     rcsw.getEndpointsPorts(exportedService),
 			},
 		}
 	} else {
 		rcsw.log.Warnf("gateway for %s does not have ready addresses, skipping subsets", serviceInfo)
 	}
+
 	if rcsw.link.GatewayIdentity != "" {
 		endpointsToCreate.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.GatewayIdentity
 	}
 
-	rcsw.log.Infof("Creating a new service mirror for %s", serviceInfo)
-	if _, err := rcsw.localAPIClient.Client.CoreV1().Services(remoteService.Namespace).Create(ctx, serviceToCreate, metav1.CreateOptions{}); err != nil {
-		if !kerrors.IsAlreadyExists(err) {
-			// we might have created it during earlier attempt, if that is not the case, we retry
-			return RetryableError{[]error{err}}
-		}
-	}
-
-	rcsw.log.Infof("Creating a new Endpoints for %s", serviceInfo)
-	if _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(ev.service.Namespace).Create(ctx, endpointsToCreate, metav1.CreateOptions{}); err != nil {
+	rcsw.log.Infof("Creating a new endpoints for %s", serviceInfo)
+	if _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(exportedService.Namespace).Create(ctx, endpointsToCreate, metav1.CreateOptions{}); err != nil {
 		// we clean up after ourselves
-		rcsw.localAPIClient.Client.CoreV1().Services(ev.service.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{})
+		rcsw.localAPIClient.Client.CoreV1().Services(exportedService.Namespace).Delete(ctx, localServiceName, metav1.DeleteOptions{})
 		// and retry
 		return RetryableError{[]error{err}}
 	}
@@ -506,6 +580,7 @@ func (rcsw *RemoteClusterServiceWatcher) createOrUpdateService(service *corev1.S
 			}
 			return RetryableError{[]error{err}}
 		}
+
 		return nil
 	}
 	localSvc, err := rcsw.localAPIClient.Svc().Lister().Services(service.Namespace).Get(localName)
@@ -562,8 +637,12 @@ func (rcsw *RemoteClusterServiceWatcher) processNextEvent(ctx context.Context) (
 	switch ev := event.(type) {
 	case *OnAddCalled:
 		err = rcsw.createOrUpdateService(ev.svc)
+	case *OnAddEndpointsCalled:
+		err = rcsw.createOrUpdateHeadlessEndpoints(ctx, ev.ep)
 	case *OnUpdateCalled:
 		err = rcsw.createOrUpdateService(ev.svc)
+	case *OnUpdateEndpointsCalled:
+		err = rcsw.createOrUpdateHeadlessEndpoints(ctx, ev.ep)
 	case *OnDeleteCalled:
 		rcsw.handleOnDelete(ev.svc)
 	case *RemoteServiceCreated:
@@ -655,6 +734,34 @@ func (rcsw *RemoteClusterServiceWatcher) Start(ctx context.Context) error {
 			},
 		},
 	)
+	if rcsw.headlessServicesEnabled {
+		rcsw.remoteAPIClient.Endpoint().Informer().AddEventHandler(
+			cache.ResourceEventHandlerFuncs{
+				AddFunc: func(obj interface{}) {
+					if obj.(metav1.Object).GetNamespace() == "kube-system" {
+						return
+					}
+
+					if ok := isExportedHeadlessEndpoints(obj, rcsw.log); !ok {
+						return
+					}
+
+					rcsw.eventsQueue.Add(&OnAddEndpointsCalled{obj.(*corev1.Endpoints)})
+				},
+				UpdateFunc: func(old, new interface{}) {
+					if new.(metav1.Object).GetNamespace() == "kube-system" {
+						return
+					}
+
+					if ok := isExportedHeadlessEndpoints(new, rcsw.log); !ok {
+						return
+					}
+
+					rcsw.eventsQueue.Add(&OnUpdateEndpointsCalled{new.(*corev1.Endpoints)})
+				},
+			},
+		)
+	}
 	go rcsw.processEvents(ctx)
 
 	// We need to issue a RepairEndpoints immediately to populate the gateway
@@ -760,6 +867,14 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) er
 	for _, svc := range mirrorServices {
 		updatedService := svc.DeepCopy()
 
+		// If the Service is headless we should skip repairing its Endpoints.
+		// Headless Services that are mirrored on a remote cluster will have
+		// their Endpoints created with hostnames and nested clusterIP services,
+		// we should avoid replacing these with the gateway address.
+		if svc.Spec.ClusterIP == corev1.ClusterIPNone {
+			rcsw.log.Debugf("Skipped repairing endpoints for %s/%s", svc.Namespace, svc.Name)
+			continue
+		}
 		endpoints, err := rcsw.localAPIClient.Endpoint().Lister().Endpoints(svc.Namespace).Get(svc.Name)
 		if err != nil {
 			rcsw.log.Errorf("Could not get endpoints: %s", err)
@@ -815,3 +930,422 @@ func (rcsw *RemoteClusterServiceWatcher) createOrUpdateEndpoints(ctx context.Con
 
 	return nil
 }
+
+// createOrUpdateHeadlessEndpoints processes endpoints objects for exported
+// headless services. When an endpoints object is created or updated in the
+// remote cluster, it will be processed here in order to reconcile the local
+// cluster state with the remote cluster state.
+//
+// createOrUpdateHeadlessEndpoints is also responsible for creating the service
+// mirror in the source cluster. In order for an exported headless service to be
+// mirrored as headless, it must have at least one port defined and at least one
+// named address in its endpoints object (e.g a deployment would not work since
+// pods may not have arbitrary hostnames). As such, when an endpoints object is
+// first processed, if there is no mirror service, we create one, by looking at
+// the endpoints object itself. If the exported service is deemed to be valid
+// for headless mirroring, then the function will create the headless mirror and
+// then create an endpoints object for it in the source cluster. If it is not
+// valid, the exported service will be mirrored as clusterIP and its endpoints
+// will point to the gateway.
+//
+// When creating endpoints for a headless mirror, we also create an endpoint
+// mirror (clusterIP) service for each of the endpoints' named addresses. If the
+// headless mirror exists and has an endpoints object, we simply update by
+// either creating or deleting endpoint mirror services.
+func (rcsw *RemoteClusterServiceWatcher) createOrUpdateHeadlessEndpoints(ctx context.Context, exportedEndpoints *corev1.Endpoints) error {
+	exportedService, err := rcsw.remoteAPIClient.Svc().Lister().Services(exportedEndpoints.Namespace).Get(exportedEndpoints.Name)
+	if err != nil {
+		rcsw.log.Debugf("failed to retrieve exported service %s/%s when updating its headless mirror endpoints: %v", exportedEndpoints.Namespace, exportedEndpoints.Name, err)
+		return fmt.Errorf("error retrieving exported service %s/%s: %v", exportedEndpoints.Namespace, exportedEndpoints.Name, err)
+	}
+
+	// Check whether the endpoints should be processed for a headless exported
+	// service. If the exported service does not have any ports exposed, then
+	// neither will its corresponding endpoint mirrors, it should not be created
+	// as a headless mirror. If the service does not have any named addresses in
+	// its Endpoints object, then the endpoints should not be processed.
+	if len(exportedService.Spec.Ports) == 0 {
+		rcsw.recorder.Event(exportedService, v1.EventTypeNormal, eventTypeSkipped, "Skipped mirroring service: object spec has no exposed ports")
+		rcsw.log.Infof("Skipped creating headless mirror for %s/%s: service object spec has no exposed ports", exportedService.Namespace, exportedService.Name)
+		return nil
+	}
+
+	mirrorServiceName := rcsw.mirroredResourceName(exportedService.Name)
+	mirrorService, err := rcsw.localAPIClient.Svc().Lister().Services(exportedService.Namespace).Get(mirrorServiceName)
+	if err != nil {
+		if !kerrors.IsNotFound(err) {
+			return err
+		}
+
+		// If the mirror service does not exist, create it, either as clusterIP
+		// or as headless.
+		mirrorService, err = rcsw.createRemoteHeadlessService(ctx, exportedService, exportedEndpoints)
+		if err != nil {
+			return err
+		}
+	}
+
+	headlessMirrorEpName := rcsw.mirroredResourceName(exportedEndpoints.Name)
+	headlessMirrorEndpoints, err := rcsw.localAPIClient.Endpoint().Lister().Endpoints(exportedEndpoints.Namespace).Get(headlessMirrorEpName)
+	if err != nil {
+		if !kerrors.IsNotFound(err) {
+			return err
+		}
+
+		if mirrorService.Spec.ClusterIP != corev1.ClusterIPNone {
+			return rcsw.createGatewayEndpoints(ctx, exportedService)
+		}
+
+		// Create endpoint mirrors for headless mirror
+		if err := rcsw.createHeadlessMirrorEndpoints(ctx, exportedService, exportedEndpoints); err != nil {
+			rcsw.log.Debugf("failed to create headless mirrors for endpoints %s/%s: %v", exportedEndpoints.Namespace, exportedEndpoints.Name, err)
+			return err
+		}
+
+		return nil
+	}
+
+	// Past this point, we do not want to process a mirror service that is not
+	// headless. We want to process only endpoints for headless mirrors; before
+	// this point it would have been possible to have a clusterIP mirror, since
+	// we are creating the mirror service in the scope of the function.
+	if mirrorService.Spec.ClusterIP != corev1.ClusterIPNone {
+		return nil
+	}
+
+	mirrorEndpoints := headlessMirrorEndpoints.DeepCopy()
+	endpointMirrors := make(map[string]struct{})
+	newSubsets := make([]corev1.EndpointSubset, 0, len(exportedEndpoints.Subsets))
+	for _, subset := range exportedEndpoints.Subsets {
+		newAddresses := make([]corev1.EndpointAddress, 0, len(subset.Addresses))
+		for _, address := range subset.Addresses {
+			if address.Hostname == "" {
+				continue
+			}
+
+			endpointMirrorName := rcsw.mirroredResourceName(address.Hostname)
+			endpointMirrorService, err := rcsw.localAPIClient.Svc().Lister().Services(exportedEndpoints.Namespace).Get(endpointMirrorName)
+			if err != nil {
+				if !kerrors.IsNotFound(err) {
+					return err
+				}
+				// If the error is 'NotFound' then the Endpoint Mirror service
+				// does not exist, so create it.
+				endpointMirrorService, err = rcsw.createEndpointMirrorService(ctx, address.Hostname, exportedEndpoints.ResourceVersion, endpointMirrorName, exportedService)
+				if err != nil {
+					return err
+				}
+			}
+
+			endpointMirrors[endpointMirrorName] = struct{}{}
+			newAddresses = append(newAddresses, corev1.EndpointAddress{
+				Hostname: address.Hostname,
+				IP:       endpointMirrorService.Spec.ClusterIP,
+			})
+		}
+
+		if len(newAddresses) == 0 {
+			continue
+		}
+
+		// copy ports, create subset
+		newSubsets = append(newSubsets, corev1.EndpointSubset{
+			Addresses: newAddresses,
+			Ports:     subset.DeepCopy().Ports,
+		})
+	}
+
+	headlessMirrorName := rcsw.mirroredResourceName(exportedService.Name)
+	matchLabels := map[string]string{
+		consts.MirroredHeadlessSvcNameLabel: headlessMirrorName,
+	}
+
+	// Fetch all Endpoint Mirror services that belong to the same Headless Mirror
+	endpointMirrorServices, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
+	if err != nil {
+		return err
+	}
+
+	var errors []error
+	for _, service := range endpointMirrorServices {
+		// If the service's name does not show up in the up-to-date map of
+		// Endpoint Mirror names, then we should delete it.
+		if _, found := endpointMirrors[service.Name]; found {
+			continue
+		}
+		err := rcsw.localAPIClient.Client.CoreV1().Services(service.Namespace).Delete(ctx, service.Name, metav1.DeleteOptions{})
+		if err != nil {
+			if !kerrors.IsNotFound(err) {
+				errors = append(errors, fmt.Errorf("error deleting Endpoint Mirror service %s/%s: %v", service.Namespace, service.Name, err))
+			}
+		}
+	}
+
+	if len(errors) > 0 {
+		return RetryableError{errors}
+	}
+
+	// Update
+	mirrorEndpoints.Subsets = newSubsets
+	_, err = rcsw.localAPIClient.Client.CoreV1().Endpoints(mirrorEndpoints.Namespace).Update(ctx, mirrorEndpoints, metav1.UpdateOptions{})
+	if err != nil {
+		return RetryableError{[]error{err}}
+	}
+
+	return nil
+}
+
+// createRemoteHeadlessService creates a mirror service for an exported headless
+// service. Whether the mirror will be created as a headless or clusterIP
+// service depends on the endpoints object associated with the exported service.
+// If there is at least one named address, then the service will be mirrored as
+// headless.
+//
+// Note: we do not check for any exposed ports because it was previously done
+// when the service was picked up by the service mirror. We also do not need to
+// check if the exported service is headless; its endpoints will be processed
+// only if it is headless so we are certain at this point that is the case.
+func (rcsw *RemoteClusterServiceWatcher) createRemoteHeadlessService(ctx context.Context, exportedService *corev1.Service, exportedEndpoints *corev1.Endpoints) (*corev1.Service, error) {
+	// If we don't have any subsets to process then avoid creating the service.
+	// We need at least one address to be make a decision (whether we should
+	// create as clusterIP or headless), rely on the endpoints being eventually
+	// consistent, will likely receive an update with subsets.
+	if len(exportedEndpoints.Subsets) == 0 {
+		return &corev1.Service{}, nil
+	}
+
+	remoteService := exportedService.DeepCopy()
+	serviceInfo := fmt.Sprintf("%s/%s", remoteService.Namespace, remoteService.Name)
+	localServiceName := rcsw.mirroredResourceName(remoteService.Name)
+
+	if err := rcsw.mirrorNamespaceIfNecessary(ctx, remoteService.Namespace); err != nil {
+		return &corev1.Service{}, err
+	}
+
+	serviceToCreate := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        localServiceName,
+			Namespace:   remoteService.Namespace,
+			Annotations: rcsw.getMirroredServiceAnnotations(remoteService),
+			Labels:      rcsw.getMirroredServiceLabels(),
+		},
+		Spec: corev1.ServiceSpec{
+			Ports: remapRemoteServicePorts(remoteService.Spec.Ports),
+		},
+	}
+
+	if shouldExportAsHeadlessService(exportedEndpoints, rcsw.log) {
+		serviceToCreate.Spec.ClusterIP = corev1.ClusterIPNone
+		rcsw.log.Infof("Creating a new headless service mirror for %s", serviceInfo)
+	} else {
+		rcsw.log.Infof("Creating a new service mirror for %s", serviceInfo)
+	}
+
+	svc, err := rcsw.localAPIClient.Client.CoreV1().Services(remoteService.Namespace).Create(ctx, serviceToCreate, metav1.CreateOptions{})
+	if err != nil {
+		if !kerrors.IsAlreadyExists(err) {
+			// we might have created it during earlier attempt, if that is not the case, we retry
+			return &corev1.Service{}, RetryableError{[]error{err}}
+		}
+	}
+
+	return svc, err
+}
+
+// createHeadlessMirrorEndpoints creates an endpoints object for a Headless
+// Mirror service. The endpoints object will contain the same subsets and hosts
+// as the endpoints object of the exported headless service. Each host in the
+// Headless Mirror's endpoints object will point to an Endpoint Mirror service.
+func (rcsw *RemoteClusterServiceWatcher) createHeadlessMirrorEndpoints(ctx context.Context, exportedService *corev1.Service, exportedEndpoints *corev1.Endpoints) error {
+	exportedServiceInfo := fmt.Sprintf("%s/%s", exportedService.Namespace, exportedService.Name)
+	endpointsHostnames := make(map[string]struct{})
+	subsetsToCreate := make([]corev1.EndpointSubset, 0, len(exportedEndpoints.Subsets))
+	for _, subset := range exportedEndpoints.Subsets {
+		newAddresses := make([]corev1.EndpointAddress, 0, len(subset.Addresses))
+		for _, addr := range subset.Addresses {
+			if addr.Hostname == "" {
+				continue
+			}
+
+			endpointMirrorName := rcsw.mirroredResourceName(addr.Hostname)
+			createdService, err := rcsw.createEndpointMirrorService(ctx, addr.Hostname, exportedEndpoints.ResourceVersion, endpointMirrorName, exportedService)
+			if err != nil {
+				rcsw.log.Errorf("error creating endpoint mirror service %s/%s for exported headless service %s: %v", endpointMirrorName, exportedService.Namespace, exportedServiceInfo, err)
+				continue
+			}
+
+			endpointsHostnames[addr.Hostname] = struct{}{}
+			newAddresses = append(newAddresses, corev1.EndpointAddress{
+				Hostname: addr.TargetRef.Name,
+				IP:       createdService.Spec.ClusterIP,
+			})
+
+		}
+
+		if len(newAddresses) == 0 {
+			continue
+		}
+
+		subsetsToCreate = append(subsetsToCreate, corev1.EndpointSubset{
+			Addresses: newAddresses,
+			Ports:     subset.DeepCopy().Ports,
+		})
+	}
+
+	headlessMirrorServiceName := rcsw.mirroredResourceName(exportedService.Name)
+	headlessMirrorEndpoints := &corev1.Endpoints{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      headlessMirrorServiceName,
+			Namespace: exportedService.Namespace,
+			Labels: map[string]string{
+				consts.MirroredResourceLabel:  "true",
+				consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName,
+			},
+			Annotations: map[string]string{
+				consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", exportedService.Name, exportedService.Namespace, rcsw.link.TargetClusterDomain),
+			},
+		},
+		Subsets: subsetsToCreate,
+	}
+
+	if rcsw.link.GatewayIdentity != "" {
+		headlessMirrorEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.GatewayIdentity
+	}
+
+	rcsw.log.Infof("Creating a new headless mirror endpoints object for headless mirror %s/%s", headlessMirrorServiceName, exportedService.Namespace)
+	if _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(exportedService.Namespace).Create(ctx, headlessMirrorEndpoints, metav1.CreateOptions{}); err != nil {
+		// we clean up after ourselves
+		rcsw.localAPIClient.Client.CoreV1().Services(exportedService.Namespace).Delete(ctx, headlessMirrorServiceName, metav1.DeleteOptions{})
+		// and retry
+		return RetryableError{[]error{err}}
+	}
+
+	return nil
+}
+
+// createEndpointMirrorService creates a new Endpoint Mirror service and its
+// corresponding endpoints object. It returns the newly created Endpoint Mirror
+// service object. When a headless service is exported, we create a Headless
+// Mirror service in the source cluster and then for each hostname in the
+// exported service's endpoints object, we also create an Endpoint Mirror
+// service (and its corresponding endpoints object).
+func (rcsw *RemoteClusterServiceWatcher) createEndpointMirrorService(ctx context.Context, endpointHostname, resourceVersion, endpointMirrorName string, exportedService *corev1.Service) (*corev1.Service, error) {
+	gatewayAddresses, err := rcsw.resolveGatewayAddress()
+	if err != nil {
+		return nil, err
+	}
+
+	endpointMirrorAnnotations := map[string]string{
+		// needed to detect real changes
+		consts.RemoteResourceVersionAnnotation: resourceVersion,
+		consts.RemoteServiceFqName:             fmt.Sprintf("%s.%s.%s.svc.%s", endpointHostname, exportedService.Name, exportedService.Namespace, rcsw.link.TargetClusterDomain),
+	}
+
+	endpointMirrorLabels := rcsw.getMirroredServiceLabels()
+	mirrorServiceName := rcsw.mirroredResourceName(exportedService.Name)
+	endpointMirrorLabels[consts.MirroredHeadlessSvcNameLabel] = mirrorServiceName
+
+	// Create service spec, clusterIP
+	endpointMirrorService := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        endpointMirrorName,
+			Namespace:   exportedService.Namespace,
+			Annotations: endpointMirrorAnnotations,
+			Labels:      endpointMirrorLabels,
+		},
+		Spec: corev1.ServiceSpec{
+			Ports: remapRemoteServicePorts(exportedService.Spec.Ports),
+		},
+	}
+	endpointMirrorEndpoints := &corev1.Endpoints{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      endpointMirrorService.Name,
+			Namespace: endpointMirrorService.Namespace,
+			Labels:    endpointMirrorLabels,
+			Annotations: map[string]string{
+				consts.RemoteServiceFqName: endpointMirrorService.Annotations[consts.RemoteServiceFqName],
+			},
+		},
+		Subsets: []corev1.EndpointSubset{
+			{
+				Addresses: gatewayAddresses,
+				Ports:     rcsw.getEndpointsPorts(exportedService),
+			},
+		},
+	}
+
+	if rcsw.link.GatewayIdentity != "" {
+		endpointMirrorEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.GatewayIdentity
+	}
+
+	exportedServiceInfo := fmt.Sprintf("%s/%s", exportedService.Namespace, exportedService.Name)
+	endpointMirrorInfo := fmt.Sprintf("%s/%s", endpointMirrorService.Namespace, endpointMirrorName)
+	rcsw.log.Infof("Creating a new endpoint mirror service %s for exported headless service %s", endpointMirrorInfo, exportedServiceInfo)
+	createdService, err := rcsw.localAPIClient.Client.CoreV1().Services(endpointMirrorService.Namespace).Create(ctx, endpointMirrorService, metav1.CreateOptions{})
+	if err != nil {
+		if !kerrors.IsAlreadyExists(err) {
+			// we might have created it during earlier attempt, if that is not the case, we retry
+			return createdService, RetryableError{[]error{err}}
+		}
+	}
+
+	rcsw.log.Infof("Creating a new endpoints object for endpoint mirror service %s", endpointMirrorInfo)
+	if _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(endpointMirrorService.Namespace).Create(ctx, endpointMirrorEndpoints, metav1.CreateOptions{}); err != nil {
+		// If we cannot create an Endpoints object for the Endpoint Mirror
+		// service, then delete the Endpoint Mirror service we just created
+		rcsw.localAPIClient.Client.CoreV1().Services(endpointMirrorService.Namespace).Delete(ctx, endpointMirrorName, metav1.DeleteOptions{})
+		// and retry
+		return createdService, RetryableError{[]error{err}}
+	}
+
+	return createdService, nil
+}
+
+// shouldExportAsHeadlessService checks if an exported service should be
+// mirrored as a headless service or as a clusterIP service, based on its
+// endpoints object. For an exported service to be a headless mirror, it needs
+// to have at least one named address in its endpoints (that is, a pod with a
+// `hostname`). If the endpoints object does not contain at least one named
+// address, it should be exported as clusterIP.
+func shouldExportAsHeadlessService(endpoints *corev1.Endpoints, log *logging.Entry) bool {
+	for _, subset := range endpoints.Subsets {
+		for _, addr := range subset.Addresses {
+			if addr.Hostname != "" {
+				return true
+			}
+		}
+
+		for _, addr := range subset.NotReadyAddresses {
+			if addr.Hostname != "" {
+				return true
+			}
+		}
+	}
+	log.Infof("Service %s/%s should not be exported as headless: no named addresses in its endpoints object", endpoints.Namespace, endpoints.Name)
+	return false
+}
+
+// isExportedHeadlessEndpoints checks if an endpoints object belongs to a
+// headless exported service.
+func isExportedHeadlessEndpoints(obj interface{}, log *logging.Entry) bool {
+	ep, ok := obj.(*corev1.Endpoints)
+	if !ok {
+		log.Errorf("error processing endpoints object: got %#v, expected *corev1.Endpoints", ep)
+		return false
+	}
+
+	if _, found := ep.Labels[corev1.IsHeadlessService]; !found {
+		// Not an Endpoints object for a headless service? Then we likely don't want
+		// to update anything.
+		log.Debugf("skipped processing endpoints object %s/%s: missing %s label", ep.Namespace, ep.Name, corev1.IsHeadlessService)
+		return false
+	}
+
+	// If Endpoints belong to an unexported service, ignore.
+	if _, found := ep.Labels[consts.DefaultExportedServiceSelector]; !found {
+		log.Debugf("skipped processing endpoints object %s/%s: missing %s label", ep.Namespace, ep.Name, consts.DefaultExportedServiceSelector)
+		return false
+	}
+
+	return true
+}
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 0d8db06de..099d6f1a0 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -63,13 +63,6 @@ const (
 	// checks must be added first.
 	LinkerdPreInstallChecks CategoryID = "pre-kubernetes-setup"
 
-	// LinkerdPreInstallCapabilityChecks adds a check to validate the user has the
-	// capabilities necessary to deploy Linkerd. For example, the NET_ADMIN and
-	// NET_RAW capabilities are required by the `linkerd-init` container to modify
-	// IP tables. These checks are not run when the `--linkerd-cni-enabled` flag
-	// is set.
-	LinkerdPreInstallCapabilityChecks CategoryID = "pre-kubernetes-capability"
-
 	// LinkerdPreInstallGlobalResourcesChecks adds a series of checks to determine
 	// the existence of the global resources like cluster roles, cluster role
 	// bindings, mutating webhook configuration validating webhook configuration
@@ -631,28 +624,6 @@ func (hc *HealthChecker) allCategories() []*Category {
 			},
 			false,
 		),
-		NewCategory(
-			LinkerdPreInstallCapabilityChecks,
-			[]Checker{
-				{
-					description: "has NET_ADMIN capability",
-					hintAnchor:  "pre-k8s-cluster-net-admin",
-					warning:     true,
-					check: func(ctx context.Context) error {
-						return hc.checkCapability(ctx, "NET_ADMIN")
-					},
-				},
-				{
-					description: "has NET_RAW capability",
-					hintAnchor:  "pre-k8s-cluster-net-raw",
-					warning:     true,
-					check: func(ctx context.Context) error {
-						return hc.checkCapability(ctx, "NET_RAW")
-					},
-				},
-			},
-			false,
-		),
 		NewCategory(
 			LinkerdPreInstallGlobalResourcesChecks,
 			[]Checker{
@@ -691,13 +662,6 @@ func (hc *HealthChecker) allCategories() []*Category {
 						return hc.checkValidatingWebhookConfigurations(ctx, false)
 					},
 				},
-				{
-					description: "no PodSecurityPolicies exist",
-					hintAnchor:  "pre-l5d-existence",
-					check: func(ctx context.Context) error {
-						return hc.checkPodSecurityPolicies(ctx, false)
-					},
-				},
 			},
 			false,
 		),
@@ -843,14 +807,6 @@ func (hc *HealthChecker) allCategories() []*Category {
 						return hc.checkValidatingWebhookConfigurations(ctx, true)
 					},
 				},
-				{
-					description: "control plane PodSecurityPolicies exist",
-					hintAnchor:  "l5d-existence-psp",
-					fatal:       true,
-					check: func(ctx context.Context) error {
-						return hc.checkPodSecurityPolicies(ctx, true)
-					},
-				},
 			},
 			false,
 		),
@@ -869,22 +825,6 @@ func (hc *HealthChecker) allCategories() []*Category {
 						return err
 					},
 				},
-				{
-					description: "cni plugin PodSecurityPolicy exists",
-					hintAnchor:  "cni-plugin-psp-exists",
-					fatal:       true,
-					check: func(ctx context.Context) error {
-						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
-						}
-						pspName := fmt.Sprintf("linkerd-%s-cni", hc.CNINamespace)
-						_, err := hc.kubeAPI.PolicyV1beta1().PodSecurityPolicies().Get(ctx, pspName, metav1.GetOptions{})
-						if kerrors.IsNotFound(err) {
-							return fmt.Errorf("missing PodSecurityPolicy: %s", pspName)
-						}
-						return err
-					},
-				},
 				{
 					description: "cni plugin ClusterRole exists",
 					hintAnchor:  "cni-plugin-cr-exists",
@@ -915,36 +855,6 @@ func (hc *HealthChecker) allCategories() []*Category {
 						return err
 					},
 				},
-				{
-					description: "cni plugin Role exists",
-					hintAnchor:  "cni-plugin-r-exists",
-					fatal:       true,
-					check: func(ctx context.Context) error {
-						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
-						}
-						_, err := hc.kubeAPI.RbacV1().Roles(hc.CNINamespace).Get(ctx, linkerdCNIResourceName, metav1.GetOptions{})
-						if kerrors.IsNotFound(err) {
-							return fmt.Errorf("missing Role: %s", linkerdCNIResourceName)
-						}
-						return err
-					},
-				},
-				{
-					description: "cni plugin RoleBinding exists",
-					hintAnchor:  "cni-plugin-rb-exists",
-					fatal:       true,
-					check: func(ctx context.Context) error {
-						if !hc.CNIEnabled {
-							return &SkipError{Reason: linkerdCNIDisabledSkipReason}
-						}
-						_, err := hc.kubeAPI.RbacV1().RoleBindings(hc.CNINamespace).Get(ctx, linkerdCNIResourceName, metav1.GetOptions{})
-						if kerrors.IsNotFound(err) {
-							return fmt.Errorf("missing RoleBinding: %s", linkerdCNIResourceName)
-						}
-						return err
-					},
-				},
 				{
 					description: "cni plugin ServiceAccount exists",
 					hintAnchor:  "cni-plugin-sa-exists",
@@ -2137,23 +2047,6 @@ func (hc *HealthChecker) checkValidatingWebhookConfigurations(ctx context.Contex
 	return checkResources("ValidatingWebhookConfigurations", objects, []string{k8s.SPValidatorWebhookConfigName}, shouldExist)
 }
 
-func (hc *HealthChecker) checkPodSecurityPolicies(ctx context.Context, shouldExist bool) error {
-	options := metav1.ListOptions{
-		LabelSelector: hc.controlPlaneComponentsSelector(),
-	}
-	psp, err := hc.kubeAPI.PolicyV1beta1().PodSecurityPolicies().List(ctx, options)
-	if err != nil {
-		return err
-	}
-
-	objects := []runtime.Object{}
-	for _, item := range psp.Items {
-		item := item // pin
-		objects = append(objects, &item)
-	}
-	return checkResources("PodSecurityPolicies", objects, []string{fmt.Sprintf("linkerd-%s-control-plane", hc.ControlPlaneNamespace)}, shouldExist)
-}
-
 // MeshedPodIdentityData contains meshed pod details + trust anchors of the proxy
 type MeshedPodIdentityData struct {
 	Name      string
@@ -2451,48 +2344,6 @@ func (hc *HealthChecker) checkCanGet(ctx context.Context, namespace, group, vers
 	return CheckCanPerformAction(ctx, hc.kubeAPI, "get", namespace, group, version, resource)
 }
 
-func (hc *HealthChecker) checkCapability(ctx context.Context, cap string) error {
-	if hc.kubeAPI == nil {
-		// we should never get here
-		return fmt.Errorf("unexpected error: Kubernetes ClientSet not initialized")
-	}
-
-	pspList, err := hc.kubeAPI.PolicyV1beta1().PodSecurityPolicies().List(ctx, metav1.ListOptions{})
-	if err != nil {
-		return err
-	}
-
-	if len(pspList.Items) == 0 {
-		// no PodSecurityPolicies found, assume PodSecurityPolicy admission controller is disabled
-		return nil
-	}
-
-	// if PodSecurityPolicies are found, validate one exists that:
-	// 1) permits usage
-	// AND
-	// 2) provides the specified capability
-	for _, psp := range pspList.Items {
-		err := k8s.ResourceAuthz(
-			ctx,
-			hc.kubeAPI,
-			"",
-			"use",
-			"policy",
-			"v1beta1",
-			"podsecuritypolicies",
-			psp.GetName(),
-		)
-		if err == nil {
-			for _, capability := range psp.Spec.AllowedCapabilities {
-				if capability == "*" || string(capability) == cap {
-					return nil
-				}
-			}
-		}
-	}
-
-	return fmt.Errorf("found %d PodSecurityPolicies, but none provide %s, proxy injection will fail if the PSP admission controller is running", len(pspList.Items), cap)
-}
 func (hc *HealthChecker) checkExtensionAPIServerAuthentication(ctx context.Context) error {
 	if hc.kubeAPI == nil {
 		return fmt.Errorf("unexpected error: Kubernetes ClientSet not initialized")
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index 06b764489..0d99e8b55 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -410,6 +410,10 @@ const (
 	// MirroredGatewayLabel indicates that this is a mirrored gateway
 	MirroredGatewayLabel = SvcMirrorPrefix + "/mirrored-gateway"
 
+	// MirroredHeadlessSvcNameLabel indicates the root headless service for
+	// mirrored headless hosts.
+	MirroredHeadlessSvcNameLabel = SvcMirrorPrefix + "/headless-mirror-svc-name"
+
 	// RemoteClusterNameLabel put on a local mirrored service, it
 	// allows us to associate a mirrored service with a remote cluster
 	RemoteClusterNameLabel = SvcMirrorPrefix + "/cluster-name"
diff --git a/pkg/k8s/resource/resource.go b/pkg/k8s/resource/resource.go
index ecb2559d4..951bfbec2 100644
--- a/pkg/k8s/resource/resource.go
+++ b/pkg/k8s/resource/resource.go
@@ -8,7 +8,6 @@ import (
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	admissionRegistration "k8s.io/api/admissionregistration/v1"
 	core "k8s.io/api/core/v1"
-	policy "k8s.io/api/policy/v1beta1"
 	rbac "k8s.io/api/rbac/v1"
 	apiextension "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -113,12 +112,6 @@ func FetchKubernetesResources(ctx context.Context, k *k8s.KubernetesAPI, options
 	}
 	resources = append(resources, apiCRDs...)
 
-	psps, err := fetchPodSecurityPolicy(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch PodSecurityPolicy resources:%v", err)
-	}
-	resources = append(resources, psps...)
-
 	mutatinghooks, err := fetchMutatingWebhooksConfiguration(ctx, k, options)
 	if err != nil {
 		return nil, fmt.Errorf("could not fetch MutatingWebhookConfigurations:%v", err)
@@ -227,20 +220,6 @@ func fetchNamespace(ctx context.Context, k *k8s.KubernetesAPI, options metav1.Li
 	return resources, nil
 }
 
-func fetchPodSecurityPolicy(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
-	list, err := k.PolicyV1beta1().PodSecurityPolicies().List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		resources[i] = New(policy.SchemeGroupVersion.String(), "PodSecurityPolicy", item.Name)
-	}
-
-	return resources, nil
-}
-
 func fetchValidatingWebhooksConfiguration(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
 	list, err := k.AdmissionregistrationV1().ValidatingWebhookConfigurations().List(ctx, options)
 	if err != nil {
diff --git a/viz/charts/linkerd-viz/README.md b/viz/charts/linkerd-viz/README.md
index f681f6265..36634c7a5 100644
--- a/viz/charts/linkerd-viz/README.md
+++ b/viz/charts/linkerd-viz/README.md
@@ -98,6 +98,7 @@ Kubernetes: `>=1.16.0-0`
 | defaultLogLevel | string | `"info"` | Log level for all the viz components |
 | defaultRegistry | string | `"cr.l5d.io/linkerd"` | Docker registry for all viz components |
 | defaultUID | int | `2103` | UID for all the viz components |
+| enablePSP | bool | `false` | Create Roles and RoleBindings to associate this extension's ServiceAccounts to the control plane PSP resource. This requires that `enabledPSP` is set to true on the control plane install. Note PSP has been deprecated since k8s v1.21 |
 | enablePodAntiAffinity | bool | `false` | Enables Pod Anti Affinity logic to balance the placement of replicas across hosts and zones for High Availability. Enable this only when you have multiple replicas of components. |
 | grafana.enabled | bool | `true` | toggle field to enable or disable grafana |
 | grafana.image.name | string | `"grafana"` | Docker image name for the grafana instance |
diff --git a/viz/charts/linkerd-viz/templates/psp.yaml b/viz/charts/linkerd-viz/templates/psp.yaml
index 444c7e9a1..b0bc1d60e 100644
--- a/viz/charts/linkerd-viz/templates/psp.yaml
+++ b/viz/charts/linkerd-viz/templates/psp.yaml
@@ -1,3 +1,4 @@
+{{ if .Values.enablePSP -}}
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -48,3 +49,4 @@ subjects:
 - kind: ServiceAccount
   name: tap-injector
   namespace: {{.Values.namespace}}
+{{ end -}}
diff --git a/viz/charts/linkerd-viz/templates/web-rbac.yaml b/viz/charts/linkerd-viz/templates/web-rbac.yaml
index 15ad2531c..300a3d1cd 100644
--- a/viz/charts/linkerd-viz/templates/web-rbac.yaml
+++ b/viz/charts/linkerd-viz/templates/web-rbac.yaml
@@ -64,9 +64,6 @@ rules:
 - apiGroups: ["admissionregistration.k8s.io"]
   resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"]
   verbs: ["list"]
-- apiGroups: ["policy"]
-  resources: ["podsecuritypolicies"]
-  verbs: ["list"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
diff --git a/viz/charts/linkerd-viz/values.yaml b/viz/charts/linkerd-viz/values.yaml
index b6fd5421f..0e7db7047 100644
--- a/viz/charts/linkerd-viz/values.yaml
+++ b/viz/charts/linkerd-viz/values.yaml
@@ -48,6 +48,12 @@ tolerations: &default_tolerations
 # Enable this only when you have multiple replicas of components.
 enablePodAntiAffinity: false
 
+# -- Create Roles and RoleBindings to associate this extension's
+# ServiceAccounts to the control plane PSP resource. This requires that
+# `enabledPSP` is set to true on the control plane install. Note PSP has been
+# deprecated since k8s v1.21
+enablePSP: false
+
 # -- url of external prometheus instance
 prometheusUrl: ""
 
diff --git a/web/app/package.json b/web/app/package.json
index 9e6950b33..63f8377b7 100644
--- a/web/app/package.json
+++ b/web/app/package.json
@@ -86,7 +86,7 @@
     "url-loader": "^4.1.1",
     "wdio-chromedriver-service": "6.0.4",
     "webdriverio": "7.8.0",
-    "webpack": "^5.46.0",
+    "webpack": "^5.47.1",
     "webpack-bundle-analyzer": "4.4.2",
     "webpack-cli": "4.7.2",
     "webpack-dev-server": "3.11.2"
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index b75bcb19a..794c67554 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -2030,16 +2030,6 @@
     deepmerge "^4.0.0"
     glob "^7.1.2"
 
-"@wdio/config@7.7.7":
-  version "7.7.7"
-  resolved "https://registry.yarnpkg.com/@wdio/config/-/config-7.7.7.tgz#74fe17d84dddb6a3f3eb2ee689691a3e429e6af4"
-  integrity sha512-uuC2Fw8nXwFMi3BhoKjpvGkONCQkAJO+STcgQFx7G3OdubdP14OaFLFPqXuKTghcnHQhbc/RGIunIi3bigNt4w==
-  dependencies:
-    "@wdio/logger" "7.7.0"
-    "@wdio/types" "7.7.7"
-    deepmerge "^4.0.0"
-    glob "^7.1.2"
-
 "@wdio/config@7.8.0":
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/@wdio/config/-/config-7.8.0.tgz#64d644fe70c44a9c08809fc9485a0fab34f3bb69"
@@ -2134,13 +2124,6 @@
   dependencies:
     "@wdio/utils" "7.0.0"
 
-"@wdio/repl@7.7.7":
-  version "7.7.7"
-  resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-7.7.7.tgz#d4f0e54cfbbdf48abc1f1dda8d73db7373ba8f2b"
-  integrity sha512-ZMIfyeOp9pojT8J4FKK1JVC8rb9lmnI5LIiWGLLYoyAPRuVwnTLD5uCHZCFw1rOJUS4N4qre/furKJ57ZovDHg==
-  dependencies:
-    "@wdio/utils" "7.7.7"
-
 "@wdio/repl@7.8.0":
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-7.8.0.tgz#fcfb7023cdc1c1f78532467bca4ee4e0e7f82ade"
@@ -2181,14 +2164,6 @@
   dependencies:
     got "^11.8.1"
 
-"@wdio/types@7.7.7":
-  version "7.7.7"
-  resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.7.7.tgz#cc5c74f926ebb0771507bc83247d61a41eb08602"
-  integrity sha512-E1enStCkdsb1lvTxCXU2ITCP+hHpVFt3lttadeiJxpkhIPIlu9Qfk8+Fz9IOhxS+NBBLrFq67f3+SA4GtK3JiQ==
-  dependencies:
-    "@types/node" "^15.12.5"
-    got "^11.8.1"
-
 "@wdio/types@7.8.0":
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.8.0.tgz#03ead5b8d10fb0d373bebe16a92c8c10e4e01059"
@@ -2212,14 +2187,6 @@
     "@wdio/logger" "7.0.0"
     "@wdio/types" "7.0.0"
 
-"@wdio/utils@7.7.7":
-  version "7.7.7"
-  resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-7.7.7.tgz#70c7986178d598d1dabf400df92a5b8c76799c37"
-  integrity sha512-va2pr4MCOD3VvuZ7p3mf8Nw/l2NhHlOVVhNNrSeKonGHLPrr2Th1jKI1ZJwqsEsMJmGBP0F3yFPdenuNJUxPbQ==
-  dependencies:
-    "@wdio/logger" "7.7.0"
-    "@wdio/types" "7.7.7"
-
 "@wdio/utils@7.8.0":
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-7.8.0.tgz#91b569cad30af2bb54ff94db87e84e9a1f92bca5"
@@ -4271,24 +4238,6 @@ devtools@7.0.0:
     ua-parser-js "^0.7.21"
     uuid "^8.0.0"
 
-devtools@7.7.8:
-  version "7.7.8"
-  resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.7.8.tgz#4ddf9f3516ff46b8d1e558d89a35342ce27fa7bd"
-  integrity sha512-BVo8DgdERZC+XeljU3F6pFxvkdHwIRl8Vfnh2vUEx393D1j4FA6Fgm5mPAEmB/ZmVr6DVPGBNc/00qneM/DPxA==
-  dependencies:
-    "@types/node" "^15.12.5"
-    "@wdio/config" "7.7.7"
-    "@wdio/logger" "7.7.0"
-    "@wdio/protocols" "7.7.4"
-    "@wdio/types" "7.7.7"
-    "@wdio/utils" "7.7.7"
-    chrome-launcher "^0.14.0"
-    edge-paths "^2.1.0"
-    puppeteer-core "^10.1.0"
-    query-selector-shadow-dom "^1.0.0"
-    ua-parser-js "^0.7.21"
-    uuid "^8.0.0"
-
 devtools@7.8.0:
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.8.0.tgz#db9dcfbb782883eb883d683b4412882fc2d6d021"
@@ -10091,11 +10040,6 @@ sockjs@^0.3.21:
     uuid "^3.4.0"
     websocket-driver "^0.7.4"
 
-source-list-map@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
-  integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
-
 source-map-js@^0.6.2:
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
@@ -11102,20 +11046,6 @@ webdriver@7.0.0:
     got "^11.0.2"
     lodash.merge "^4.6.1"
 
-webdriver@7.7.7:
-  version "7.7.7"
-  resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.7.7.tgz#6eaac0ed4f2a1fb9ff4e6d2c0c694a2e871bdebc"
-  integrity sha512-0ZOnFFoeVVPebIX9fU3BoTMEukITgmTz9x7AcE98r211Sdx5Xa+8W5Ki9kQTihJcnBFwv+NVoYU12TMiQFzDtQ==
-  dependencies:
-    "@types/node" "^15.12.5"
-    "@wdio/config" "7.7.7"
-    "@wdio/logger" "7.7.0"
-    "@wdio/protocols" "7.7.4"
-    "@wdio/types" "7.7.7"
-    "@wdio/utils" "7.7.7"
-    got "^11.0.2"
-    lodash.merge "^4.6.1"
-
 webdriver@7.8.0:
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.8.0.tgz#ace1e8ecbb746023353beeeefdec57545c6508a8"
@@ -11192,41 +11122,6 @@ webdriverio@7.0.0:
     serialize-error "^8.0.0"
     webdriver "7.0.0"
 
-webdriverio@7.7.8:
-  version "7.7.8"
-  resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.7.8.tgz#d10d2c1ad224a302a442e179b9918640fef21713"
-  integrity sha512-v4BcH8WVX77TsYsHg+kGPg0/QxfpAh8A3KCe+3feCkL8P5ys4RNwb0L9GChJyGIE0LO0ZBj7VtTuMs+HNowreg==
-  dependencies:
-    "@types/aria-query" "^4.2.1"
-    "@types/node" "^15.12.5"
-    "@wdio/config" "7.7.7"
-    "@wdio/logger" "7.7.0"
-    "@wdio/protocols" "7.7.4"
-    "@wdio/repl" "7.7.7"
-    "@wdio/types" "7.7.7"
-    "@wdio/utils" "7.7.7"
-    archiver "^5.0.0"
-    aria-query "^4.2.2"
-    atob "^2.1.2"
-    css-shorthand-properties "^1.1.1"
-    css-value "^0.0.1"
-    devtools "7.7.8"
-    devtools-protocol "^0.0.901419"
-    fs-extra "^10.0.0"
-    get-port "^5.1.1"
-    grapheme-splitter "^1.0.2"
-    lodash.clonedeep "^4.5.0"
-    lodash.isobject "^3.0.2"
-    lodash.isplainobject "^4.0.6"
-    lodash.zip "^4.2.0"
-    minimatch "^3.0.4"
-    puppeteer-core "^10.1.0"
-    query-selector-shadow-dom "^1.0.0"
-    resq "^1.9.1"
-    rgb2hex "0.2.5"
-    serialize-error "^8.0.0"
-    webdriver "7.7.7"
-
 webdriverio@7.8.0:
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.8.0.tgz#9bc24f2301faa987b82327e0000d3bbbaeb38d5b"
@@ -11377,18 +11272,15 @@ webpack-merge@^5.7.3:
     clone-deep "^4.0.1"
     wildcard "^2.0.0"
 
-webpack-sources@^2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd"
-  integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==
-  dependencies:
-    source-list-map "^2.0.1"
-    source-map "^0.6.1"
+webpack-sources@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.1.1.tgz#586d15bc9a9723765f6a735f672357d6402f9c57"
+  integrity sha512-ztUmIWq0LWaw+1YyR3bXtUPjt8vQedtI9WxGn/q1V1ASHsombnaso7MN9S25lzKS/OuC9Q8lEg3GsZexjDbdlQ==
 
-webpack@^5.46.0:
-  version "5.46.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.46.0.tgz#105d20d96f79db59b316b0ae54316f0f630314b5"
-  integrity sha512-qxD0t/KTedJbpcXUmvMxY5PUvXDbF8LsThCzqomeGaDlCA6k998D8yYVwZMvO8sSM3BTEOaD4uzFniwpHaTIJw==
+webpack@^5.47.1:
+  version "5.47.1"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.47.1.tgz#20fb7d76f68912a2249a6dd7ff16faa178049ad2"
+  integrity sha512-cW+Mzy9SCDapFV4OrkHuP6EFV2mAsiQd+gOa3PKtHNoKg6qPqQXZzBlHH+CnQG1osplBCqwsJZ8CfGO6XWah0g==
   dependencies:
     "@types/eslint-scope" "^3.7.0"
     "@types/estree" "^0.0.50"
@@ -11412,7 +11304,7 @@ webpack@^5.46.0:
     tapable "^2.1.1"
     terser-webpack-plugin "^5.1.3"
     watchpack "^2.2.0"
-    webpack-sources "^2.3.1"
+    webpack-sources "^3.1.1"
 
 websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
   version "0.7.4"


Test patch:
diff --git a/bin/_test-helpers.sh b/bin/_test-helpers.sh
index 94cc7611f..eecf7f600 100644
--- a/bin/_test-helpers.sh
+++ b/bin/_test-helpers.sh
@@ -263,7 +263,7 @@ check_if_k8s_reachable() {
 check_if_l5d_exists() {
   printf 'Checking if Linkerd resources exist on cluster...'
   local resources
-  resources=$(kubectl --context="$context" get all,clusterrole,clusterrolebinding,mutatingwebhookconfigurations,validatingwebhookconfigurations,psp,crd -l linkerd.io/control-plane-ns --all-namespaces -oname)
+  resources=$(kubectl --context="$context" get all,clusterrole,clusterrolebinding,mutatingwebhookconfigurations,validatingwebhookconfigurations,crd -l linkerd.io/control-plane-ns --all-namespaces -oname)
   if [ -n "$resources" ]; then
     printf '
 Linkerd resources exist on cluster:
diff --git a/cli/cmd/testdata/install-cni-plugin_default.golden b/cli/cmd/testdata/install-cni-plugin_default.golden
index 0eb8073bb..f96b87b1f 100644
--- a/cli/cmd/testdata/install-cni-plugin_default.golden
+++ b/cli/cmd/testdata/install-cni-plugin_default.golden
@@ -8,28 +8,6 @@ metadata:
     linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-cni-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-spec:
-  allowPrivilegeEscalation: false
-  fsGroup:
-    rule: RunAsAny
-  hostNetwork: true
-  runAsUser:
-    rule: RunAsAny
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-  - hostPath
-  - secret
-  - emptyDir
----
 apiVersion: v1
 kind: ServiceAccount
 metadata:
@@ -38,36 +16,6 @@ metadata:
   labels:
     linkerd.io/cni-resource: "true"
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-cni
-  namespace: linkerd-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-rules:
-- apiGroups: ['extensions', 'policy']
-  resources: ['podsecuritypolicies']
-  resourceNames:
-  - linkerd-linkerd-cni-cni
-  verbs: ['use']
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-cni
-  namespace: linkerd-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: linkerd-cni
-subjects:
-- kind: ServiceAccount
-  name: linkerd-cni
-  namespace: linkerd-cni
----
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
index 88c515172..c92d411ec 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
@@ -8,28 +8,6 @@ metadata:
     linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-other-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-spec:
-  allowPrivilegeEscalation: false
-  fsGroup:
-    rule: RunAsAny
-  hostNetwork: true
-  runAsUser:
-    rule: RunAsAny
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-  - hostPath
-  - secret
-  - emptyDir
----
 apiVersion: v1
 kind: ServiceAccount
 metadata:
@@ -38,36 +16,6 @@ metadata:
   labels:
     linkerd.io/cni-resource: "true"
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-cni
-  namespace: other
-  labels:
-    linkerd.io/cni-resource: "true"
-rules:
-- apiGroups: ['extensions', 'policy']
-  resources: ['podsecuritypolicies']
-  resourceNames:
-  - linkerd-other-cni
-  verbs: ['use']
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-cni
-  namespace: other
-  labels:
-    linkerd.io/cni-resource: "true"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: linkerd-cni
-subjects:
-- kind: ServiceAccount
-  name: linkerd-cni
-  namespace: other
----
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
index 07c3dde3e..871cd583f 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
@@ -8,28 +8,6 @@ metadata:
     linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-other-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-spec:
-  allowPrivilegeEscalation: false
-  fsGroup:
-    rule: RunAsAny
-  hostNetwork: true
-  runAsUser:
-    rule: RunAsAny
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-  - hostPath
-  - secret
-  - emptyDir
----
 apiVersion: v1
 kind: ServiceAccount
 metadata:
@@ -38,36 +16,6 @@ metadata:
   labels:
     linkerd.io/cni-resource: "true"
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-cni
-  namespace: other
-  labels:
-    linkerd.io/cni-resource: "true"
-rules:
-- apiGroups: ['extensions', 'policy']
-  resources: ['podsecuritypolicies']
-  resourceNames:
-  - linkerd-other-cni
-  verbs: ['use']
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-cni
-  namespace: other
-  labels:
-    linkerd.io/cni-resource: "true"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: linkerd-cni
-subjects:
-- kind: ServiceAccount
-  name: linkerd-cni
-  namespace: other
----
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
index 6cb80ebd7..1c492227b 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
@@ -1,25 +1,3 @@
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-other-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-spec:
-  allowPrivilegeEscalation: false
-  fsGroup:
-    rule: RunAsAny
-  hostNetwork: true
-  runAsUser:
-    rule: RunAsAny
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-  - hostPath
-  - secret
-  - emptyDir
----
 apiVersion: v1
 kind: ServiceAccount
 metadata:
@@ -28,36 +6,6 @@ metadata:
   labels:
     linkerd.io/cni-resource: "true"
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-cni
-  namespace: other
-  labels:
-    linkerd.io/cni-resource: "true"
-rules:
-- apiGroups: ['extensions', 'policy']
-  resources: ['podsecuritypolicies']
-  resourceNames:
-  - linkerd-other-cni
-  verbs: ['use']
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-cni
-  namespace: other
-  labels:
-    linkerd.io/cni-resource: "true"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: linkerd-cni
-subjects:
-- kind: ServiceAccount
-  name: linkerd-cni
-  namespace: other
----
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
diff --git a/cli/cmd/testdata/install-cni-plugin_skip_ports.golden b/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
index 526fe3355..90c3d7984 100644
--- a/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
+++ b/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
@@ -8,28 +8,6 @@ metadata:
     linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-cni-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-spec:
-  allowPrivilegeEscalation: false
-  fsGroup:
-    rule: RunAsAny
-  hostNetwork: true
-  runAsUser:
-    rule: RunAsAny
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-  - hostPath
-  - secret
-  - emptyDir
----
 apiVersion: v1
 kind: ServiceAccount
 metadata:
@@ -38,36 +16,6 @@ metadata:
   labels:
     linkerd.io/cni-resource: "true"
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-cni
-  namespace: linkerd-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-rules:
-- apiGroups: ['extensions', 'policy']
-  resources: ['podsecuritypolicies']
-  resourceNames:
-  - linkerd-linkerd-cni-cni
-  verbs: ['use']
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-cni
-  namespace: linkerd-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: linkerd-cni
-subjects:
-- kind: ServiceAccount
-  name: linkerd-cni
-  namespace: linkerd-cni
----
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
diff --git a/cli/cmd/testdata/install_cni_helm_default_output.golden b/cli/cmd/testdata/install_cni_helm_default_output.golden
index 1f192d49f..11de325cf 100644
--- a/cli/cmd/testdata/install_cni_helm_default_output.golden
+++ b/cli/cmd/testdata/install_cni_helm_default_output.golden
@@ -10,28 +10,6 @@ metadata:
     linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-cni-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-spec:
-  allowPrivilegeEscalation: false
-  fsGroup:
-    rule: RunAsAny
-  hostNetwork: true
-  runAsUser:
-    rule: RunAsAny
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-  - hostPath
-  - secret
-  - emptyDir
----
 apiVersion: v1
 kind: ServiceAccount
 metadata:
@@ -40,36 +18,6 @@ metadata:
   labels:
     linkerd.io/cni-resource: "true"
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-cni
-  namespace: linkerd-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-rules:
-- apiGroups: ['extensions', 'policy']
-  resources: ['podsecuritypolicies']
-  resourceNames:
-  - linkerd-linkerd-cni-cni
-  verbs: ['use']
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-cni
-  namespace: linkerd-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: linkerd-cni
-subjects:
-- kind: ServiceAccount
-  name: linkerd-cni
-  namespace: linkerd-cni
----
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
diff --git a/cli/cmd/testdata/install_cni_helm_override_output.golden b/cli/cmd/testdata/install_cni_helm_override_output.golden
index 5ac5569f7..ed82a0278 100644
--- a/cli/cmd/testdata/install_cni_helm_override_output.golden
+++ b/cli/cmd/testdata/install_cni_helm_override_output.golden
@@ -10,28 +10,6 @@ metadata:
     linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-test-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-spec:
-  allowPrivilegeEscalation: false
-  fsGroup:
-    rule: RunAsAny
-  hostNetwork: true
-  runAsUser:
-    rule: RunAsAny
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-  - hostPath
-  - secret
-  - emptyDir
----
 apiVersion: v1
 kind: ServiceAccount
 metadata:
@@ -40,36 +18,6 @@ metadata:
   labels:
     linkerd.io/cni-resource: "true"
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-cni
-  namespace: linkerd-test
-  labels:
-    linkerd.io/cni-resource: "true"
-rules:
-- apiGroups: ['extensions', 'policy']
-  resources: ['podsecuritypolicies']
-  resourceNames:
-  - linkerd-linkerd-test-cni
-  verbs: ['use']
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-cni
-  namespace: linkerd-test
-  labels:
-    linkerd.io/cni-resource: "true"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: linkerd-cni
-subjects:
-- kind: ServiceAccount
-  name: linkerd-cni
-  namespace: linkerd-test
----
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index ebbb6698c..ab975fdce 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index 4cec5437b..2df5be094 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-l5d-control-plane
-  labels:
-    linkerd.io/control-plane-ns: l5d
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: l5d
-  labels:
-    linkerd.io/control-plane-ns: l5d
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-l5d-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: l5d
-  labels:
-    linkerd.io/control-plane-ns: l5d
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: l5d
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: l5d
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: l5d
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: l5d
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: l5d
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index a708872e9..b87fd96d4 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index ffb1892ae..51afb72a7 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
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 ce8352be0..8b884521d 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index e2a2a1fe9..26397d80f 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 2158b8f8c..b67c4c1ea 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index fd43786e8..5752fd6f1 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -627,87 +627,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index 546beeb56..31fa9f1e1 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -712,90 +712,6 @@ webhooks:
 ---
 # Source: linkerd2/templates/psp.yaml
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 # Source: linkerd2/templates/config.yaml
 ---
 kind: ConfigMap
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index 3e10f2d61..57d791fc3 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -712,90 +712,6 @@ webhooks:
 ---
 # Source: linkerd2/templates/psp.yaml
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 # Source: linkerd2/templates/config.yaml
 ---
 kind: ConfigMap
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index 13af36f14..79988b066 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -712,90 +712,6 @@ webhooks:
 ---
 # Source: linkerd2/templates/psp.yaml
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 # Source: linkerd2/templates/config.yaml
 ---
 kind: ConfigMap
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 a4230cce1..0c349fb41 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -712,90 +712,6 @@ webhooks:
 ---
 # Source: linkerd2/templates/psp.yaml
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 # Source: linkerd2/templates/config.yaml
 ---
 kind: ConfigMap
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index b680a25e2..01dff3a68 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -696,87 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: MustRunAsNonRoot
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 10001
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 10001
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index c018cd8b9..579299e58 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 0b0e44128..c06a27a20 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -696,90 +696,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-linkerd-control-plane
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: linkerd
-  labels:
-    linkerd.io/control-plane-ns: linkerd
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: linkerd
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: linkerd
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index ff7ac9838..f82c4fca9 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -682,90 +682,6 @@ webhooks:
     resources: ["pods", "services"]
   sideEffects: None
 ---
-###
-### Control Plane PSP
-###
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-l5d-control-plane
-  labels:
-    linkerd.io/control-plane-ns: l5d
-spec:
-  allowPrivilegeEscalation: false
-  readOnlyRootFilesystem: true
-  allowedCapabilities:
-  - NET_ADMIN
-  - NET_RAW
-  requiredDropCapabilities:
-  - ALL
-  hostNetwork: false
-  hostIPC: false
-  hostPID: false
-  seLinux:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  fsGroup:
-    rule: MustRunAs
-    ranges:
-    - min: 1
-      max: 65535
-  volumes:
-  - configMap
-  - emptyDir
-  - secret
-  - projected
-  - downwardAPI
-  - persistentVolumeClaim
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-psp
-  namespace: l5d
-  labels:
-    linkerd.io/control-plane-ns: l5d
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-l5d-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-psp
-  namespace: l5d
-  labels:
-    linkerd.io/control-plane-ns: l5d
-roleRef:
-  kind: Role
-  name: linkerd-psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: linkerd-destination
-  namespace: l5d
-- kind: ServiceAccount
-  name: linkerd-heartbeat
-  namespace: l5d
-- kind: ServiceAccount
-  name: linkerd-identity
-  namespace: l5d
-- kind: ServiceAccount
-  name: linkerd-proxy-injector
-  namespace: l5d
-- kind: ServiceAccount
-  name: linkerd-sp-validator
-  namespace: l5d
----
 kind: ConfigMap
 apiVersion: v1
 metadata:
diff --git a/jaeger/cmd/testdata/install_collector_disabled.golden b/jaeger/cmd/testdata/install_collector_disabled.golden
index 6b1e8033f..8ee21f00c 100644
--- a/jaeger/cmd/testdata/install_collector_disabled.golden
+++ b/jaeger/cmd/testdata/install_collector_disabled.golden
@@ -169,39 +169,6 @@ metadata:
   name: jaeger
   namespace: linkerd-jaeger
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-jaeger
-  labels:
-    linkerd.io/extension: jaeger
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: jaeger-psp
-  namespace: linkerd-jaeger
-  labels:
-    linkerd.io/extension: jaeger
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: jaeger-injector
-  namespace: linkerd-jaeger
-- kind: ServiceAccount
-  name: jaeger
-  namespace: linkerd-jaeger
----
 ###
 ### Tracing Jaeger Service
 ###
diff --git a/jaeger/cmd/testdata/install_default.golden b/jaeger/cmd/testdata/install_default.golden
index 0f79ed75f..a3c178878 100644
--- a/jaeger/cmd/testdata/install_default.golden
+++ b/jaeger/cmd/testdata/install_default.golden
@@ -178,42 +178,6 @@ metadata:
   name: jaeger
   namespace: linkerd-jaeger
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-jaeger
-  labels:
-    linkerd.io/extension: jaeger
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: jaeger-psp
-  namespace: linkerd-jaeger
-  labels:
-    linkerd.io/extension: jaeger
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: collector
-  namespace: linkerd-jaeger
-- kind: ServiceAccount
-  name: jaeger-injector
-  namespace: linkerd-jaeger
-- kind: ServiceAccount
-  name: jaeger
-  namespace: linkerd-jaeger
----
 ###
 ### Tracing Collector Service
 ###
diff --git a/jaeger/cmd/testdata/install_jaeger_disabled.golden b/jaeger/cmd/testdata/install_jaeger_disabled.golden
index 137d840a7..922ef38ea 100644
--- a/jaeger/cmd/testdata/install_jaeger_disabled.golden
+++ b/jaeger/cmd/testdata/install_jaeger_disabled.golden
@@ -169,39 +169,6 @@ webhooks:
     resources: ["pods"]
   sideEffects: None
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-jaeger
-  labels:
-    linkerd.io/extension: jaeger
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: jaeger-psp
-  namespace: linkerd-jaeger
-  labels:
-    linkerd.io/extension: jaeger
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: collector
-  namespace: linkerd-jaeger
-- kind: ServiceAccount
-  name: jaeger-injector
-  namespace: linkerd-jaeger
----
 ###
 ### Tracing Collector Service
 ###
diff --git a/multicluster/service-mirror/cluster_watcher_mirroring_test.go b/multicluster/service-mirror/cluster_watcher_mirroring_test.go
index 57fafbd38..17f0e372c 100644
--- a/multicluster/service-mirror/cluster_watcher_mirroring_test.go
+++ b/multicluster/service-mirror/cluster_watcher_mirroring_test.go
@@ -28,13 +28,13 @@ func (tc *mirroringTestCase) run(t *testing.T) {
 		if err != nil {
 			t.Fatal(err)
 		}
-
 		if tc.expectedLocalServices == nil {
 			// ensure the are no local services
 			services, err := localAPI.Client.CoreV1().Services(corev1.NamespaceAll).List(context.Background(), metav1.ListOptions{})
 			if err != nil {
 				t.Fatal(err)
 			}
+
 			if len(services.Items) > 0 {
 				t.Fatalf("Was expecting no local services but instead found %v", services.Items)
 
@@ -122,6 +122,78 @@ func TestRemoteServiceCreatedMirroring(t *testing.T) {
 				}),
 			},
 		},
+		{
+			description: "create headless service and endpoints when gateway can be resolved",
+			environment: createExportedHeadlessService,
+			expectedLocalServices: []*corev1.Service{
+				headlessMirrorService(
+					"service-one-remote",
+					"ns2",
+					"111",
+					[]corev1.ServicePort{
+						{
+							Name:     "port1",
+							Protocol: "TCP",
+							Port:     555,
+						},
+						{
+							Name:     "port2",
+							Protocol: "TCP",
+							Port:     666,
+						},
+					}),
+				endpointMirrorService(
+					"pod-0",
+					"service-one-remote",
+					"ns2",
+					"112",
+					[]corev1.ServicePort{
+						{
+							Name:     "port1",
+							Protocol: "TCP",
+							Port:     555,
+						},
+						{
+							Name:     "port2",
+							Protocol: "TCP",
+							Port:     666,
+						},
+					},
+				),
+			},
+			expectedLocalEndpoints: []*corev1.Endpoints{
+				headlessMirrorEndpoints("service-one-remote", "ns2", "pod-0", "", "gateway-identity", []corev1.EndpointPort{
+					{
+						Name:     "port1",
+						Port:     555,
+						Protocol: "TCP",
+					},
+					{
+						Name:     "port2",
+						Port:     666,
+						Protocol: "TCP",
+					},
+				}),
+				endpointMirrorEndpoints(
+					"service-one-remote",
+					"ns2",
+					"pod-0",
+					"192.0.2.129",
+					"gateway-identity",
+					[]corev1.EndpointPort{
+						{
+							Name:     "port1",
+							Port:     889,
+							Protocol: "TCP",
+						},
+						{
+							Name:     "port2",
+							Port:     889,
+							Protocol: "TCP",
+						},
+					}),
+			},
+		},
 	} {
 		tc := tt // pin
 		tc.run(t)
@@ -182,6 +254,112 @@ func TestRemoteServiceUpdatedMirroring(t *testing.T) {
 	}
 }
 
+func TestRemoteEndpointsUpdatedMirroring(t *testing.T) {
+	for _, tt := range []mirroringTestCase{
+		{
+			description: "updates headless mirror service with new remote Endpoints hosts",
+			environment: updateEndpointsWithChangedHosts,
+			expectedLocalServices: []*corev1.Service{
+				headlessMirrorService("service-two-remote", "eptest", "222", []corev1.ServicePort{
+					{
+						Name:     "port1",
+						Protocol: "TCP",
+						Port:     555,
+					},
+					{
+						Name:     "port2",
+						Protocol: "TCP",
+						Port:     666,
+					},
+				}),
+				endpointMirrorService("pod-0", "service-two-remote", "eptest", "333", []corev1.ServicePort{
+					{
+						Name:     "port1",
+						Protocol: "TCP",
+						Port:     555,
+					},
+					{
+						Name:     "port2",
+						Protocol: "TCP",
+						Port:     666,
+					},
+				}),
+				endpointMirrorService("pod-1", "service-two-remote", "eptest", "112", []corev1.ServicePort{
+					{
+						Name:     "port1",
+						Protocol: "TCP",
+						Port:     555,
+					},
+					{
+						Name:     "port2",
+						Protocol: "TCP",
+						Port:     666,
+					},
+				}),
+			},
+			expectedLocalEndpoints: []*corev1.Endpoints{
+				headlessMirrorEndpointsUpdated(
+					"service-two-remote",
+					"eptest",
+					[]string{"pod-0", "pod-1"},
+					[]string{"", ""},
+					"gateway-identity",
+					[]corev1.EndpointPort{
+						{
+							Name:     "port1",
+							Port:     555,
+							Protocol: "TCP",
+						},
+						{
+							Name:     "port2",
+							Port:     666,
+							Protocol: "TCP",
+						},
+					}),
+				endpointMirrorEndpoints(
+					"service-two-remote",
+					"eptest",
+					"pod-0",
+					"192.0.2.127",
+					"gateway-identity",
+					[]corev1.EndpointPort{
+						{
+							Name:     "port1",
+							Port:     888,
+							Protocol: "TCP",
+						},
+						{
+							Name:     "port2",
+							Port:     888,
+							Protocol: "TCP",
+						},
+					}),
+				endpointMirrorEndpoints(
+					"service-two-remote",
+					"eptest",
+					"pod-1",
+					"192.0.2.127",
+					"gateway-identity",
+					[]corev1.EndpointPort{
+						{
+							Name:     "port1",
+							Port:     888,
+							Protocol: "TCP",
+						},
+						{
+							Name:     "port2",
+							Port:     888,
+							Protocol: "TCP",
+						},
+					}),
+			},
+		},
+	} {
+		tc := tt // pin
+		tc.run(t)
+	}
+}
+
 func TestClusterUnregisteredMirroring(t *testing.T) {
 	for _, tt := range []mirroringTestCase{
 		{
diff --git a/multicluster/service-mirror/cluster_watcher_test_util.go b/multicluster/service-mirror/cluster_watcher_test_util.go
index a6b9c572c..1d889f39c 100644
--- a/multicluster/service-mirror/cluster_watcher_test_util.go
+++ b/multicluster/service-mirror/cluster_watcher_test_util.go
@@ -47,23 +47,22 @@ func (te *testEnvironment) runEnvironment(watcherQueue workqueue.RateLimitingInt
 	if err != nil {
 		return nil, err
 	}
-
 	localAPI, err := k8s.NewFakeAPI(te.localResources...)
 	if err != nil {
 		return nil, err
 	}
-
 	remoteAPI.Sync(nil)
 	localAPI.Sync(nil)
 
 	watcher := RemoteClusterServiceWatcher{
-		link:            &te.link,
-		remoteAPIClient: remoteAPI,
-		localAPIClient:  localAPI,
-		stopper:         nil,
-		log:             logging.WithFields(logging.Fields{"cluster": clusterName}),
-		eventsQueue:     watcherQueue,
-		requeueLimit:    0,
+		link:                    &te.link,
+		remoteAPIClient:         remoteAPI,
+		localAPIClient:          localAPI,
+		stopper:                 nil,
+		log:                     logging.WithFields(logging.Fields{"cluster": clusterName}),
+		eventsQueue:             watcherQueue,
+		requeueLimit:            0,
+		headlessServicesEnabled: true,
 	}
 
 	for _, ev := range te.events {
@@ -101,6 +100,7 @@ var createExportedService = &testEnvironment{
 	},
 	remoteResources: []string{
 		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "mc-gateway", 888, "gateway-identity", defaultProbePort, defaultProbePath, defaultProbePeriod),
+		endpointsAsYaml("service-one", "ns1", "192.0.2.127", "gateway-identity", []corev1.EndpointPort{}),
 	},
 	link: multicluster.Link{
 		TargetClusterName:   clusterName,
@@ -113,6 +113,82 @@ var createExportedService = &testEnvironment{
 	},
 }
 
+var createExportedHeadlessService = &testEnvironment{
+	events: []interface{}{
+		&RemoteServiceCreated{
+			service: remoteHeadlessService("service-one", "ns2", "111", map[string]string{
+				consts.DefaultExportedServiceSelector: "true",
+			}, []corev1.ServicePort{
+				{
+					Name:     "port1",
+					Protocol: "TCP",
+					Port:     555,
+				},
+				{
+					Name:     "port2",
+					Protocol: "TCP",
+					Port:     666,
+				},
+			}),
+		},
+		&OnAddEndpointsCalled{
+			ep: remoteHeadlessEndpoints("service-one", "ns2", "112", "192.0.0.1", []corev1.EndpointPort{
+				{
+					Name:     "port1",
+					Protocol: "TCP",
+					Port:     555,
+				},
+				{
+					Name:     "port2",
+					Protocol: "TCP",
+					Port:     666,
+				},
+			}),
+		},
+	},
+	remoteResources: []string{
+		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.129", "gateway", 889, "gateway-identity", 123456, "/probe1", 120),
+		remoteHeadlessSvcAsYaml("service-one", "ns2", "111",
+			[]corev1.ServicePort{
+				{
+					Name:     "port1",
+					Protocol: "TCP",
+					Port:     555,
+				},
+				{
+					Name:     "port2",
+					Protocol: "TCP",
+					Port:     666,
+				},
+			}),
+		remoteHeadlessEndpointsAsYaml("service-one", "ns2", "112", "192.0.0.1", []corev1.EndpointPort{
+			{
+				Name:     "port1",
+				Protocol: "TCP",
+				Port:     555,
+			},
+			{
+				Name:     "port2",
+				Protocol: "TCP",
+				Port:     666,
+			},
+		}),
+	},
+	link: multicluster.Link{
+		TargetClusterName:   clusterName,
+		TargetClusterDomain: clusterDomain,
+		GatewayIdentity:     "gateway-identity",
+		GatewayAddress:      "192.0.2.129",
+		GatewayPort:         889,
+		ProbeSpec: multicluster.ProbeSpec{
+			Port:   123456,
+			Path:   "/probe1",
+			Period: 120,
+		},
+		Selector: *defaultSelector,
+	},
+}
+
 var deleteMirrorService = &testEnvironment{
 	events: []interface{}{
 		&RemoteServiceDeleted{
@@ -228,6 +304,113 @@ var updateServiceWithChangedPorts = &testEnvironment{
 	},
 }
 
+var updateEndpointsWithChangedHosts = &testEnvironment{
+	events: []interface{}{
+		&OnUpdateEndpointsCalled{
+			ep: remoteHeadlessEndpointsUpdate("service-two", "eptest", "112", "192.0.0.1", []corev1.EndpointPort{
+				{
+					Name:     "port1",
+					Protocol: "TCP",
+					Port:     555,
+				},
+				{
+					Name:     "port2",
+					Protocol: "TCP",
+					Port:     666,
+				},
+			}),
+		},
+	},
+	remoteResources: []string{
+		gatewayAsYaml("gateway", "gateway-ns", "currentGatewayResVersion", "192.0.2.127", "mc-gateway", 888, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
+		remoteHeadlessSvcAsYaml("service-two", "eptest", "222",
+			[]corev1.ServicePort{
+				{
+					Name:     "port1",
+					Protocol: "TCP",
+					Port:     555,
+				},
+				{
+
+					Name:     "port2",
+					Protocol: "TCP",
+					Port:     666,
+				},
+			}),
+	},
+	localResources: []string{
+		headlessMirrorAsYaml("service-two-remote", "eptest", "222",
+			[]corev1.ServicePort{
+				{
+					Name:     "port1",
+					Protocol: "TCP",
+					Port:     555,
+				},
+				{
+					Name:     "port2",
+					Protocol: "TCP",
+					Port:     666,
+				},
+			}),
+		endpointMirrorAsYaml("pod-0", "service-two-remote", "eptest", "333", []corev1.ServicePort{
+			{
+				Name:     "port1",
+				Protocol: "TCP",
+				Port:     555,
+			},
+			{
+				Name:     "port2",
+				Protocol: "TCP",
+				Port:     666,
+			},
+		}),
+		headlessMirrorEndpointsAsYaml(
+			"service-two-remote",
+			"eptest",
+			"pod-0",
+			"",
+			"gateway-identity",
+			[]corev1.EndpointPort{
+				{
+					Name:     "port1",
+					Protocol: "TCP",
+					Port:     555,
+				},
+				{
+					Name:     "port2",
+					Protocol: "TCP",
+					Port:     666,
+				},
+			}),
+		endpointMirrorEndpointsAsYaml(
+			"service-two-remote",
+			"eptest",
+			"pod-0",
+			"192.0.2.127",
+			"gateway-identity",
+			[]corev1.EndpointPort{
+				{
+					Name:     "port1",
+					Protocol: "TCP",
+					Port:     888,
+				},
+				{
+					Name:     "port2",
+					Protocol: "TCP",
+					Port:     888,
+				},
+			}),
+	},
+	link: multicluster.Link{
+		TargetClusterName:   clusterName,
+		TargetClusterDomain: clusterDomain,
+		GatewayIdentity:     "gateway-identity",
+		GatewayAddress:      "192.0.2.127",
+		GatewayPort:         888,
+		ProbeSpec:           defaultProbeSpec,
+		Selector:            *defaultSelector,
+	},
+}
 var clusterUnregistered = &testEnvironment{
 	events: []interface{}{
 		&ClusterUnregistered{},
@@ -456,6 +639,99 @@ func remoteService(name, namespace, resourceVersion string, labels map[string]st
 	}
 }
 
+func remoteHeadlessService(name, namespace, resourceVersion string, labels map[string]string, ports []corev1.ServicePort) *corev1.Service {
+	return &corev1.Service{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Service",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:            name,
+			Namespace:       namespace,
+			ResourceVersion: resourceVersion,
+			Labels:          labels,
+		},
+		Spec: corev1.ServiceSpec{
+			ClusterIP: "None",
+			Ports:     ports,
+		},
+	}
+}
+
+func remoteHeadlessEndpoints(name, namespace, resourceVersion, address string, ports []corev1.EndpointPort) *corev1.Endpoints {
+	return &corev1.Endpoints{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Endpoints",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:            name,
+			Namespace:       namespace,
+			ResourceVersion: resourceVersion,
+			Labels: map[string]string{
+				"service.kubernetes.io/headless":      "",
+				consts.DefaultExportedServiceSelector: "true",
+			},
+		},
+		Subsets: []corev1.EndpointSubset{
+			{
+				Addresses: []corev1.EndpointAddress{
+					{
+						Hostname: "pod-0",
+						IP:       address,
+						TargetRef: &corev1.ObjectReference{
+							Name:            "pod-0",
+							ResourceVersion: resourceVersion,
+						},
+					},
+				},
+				Ports: ports,
+			},
+		},
+	}
+}
+
+func remoteHeadlessEndpointsUpdate(name, namespace, resourceVersion, address string, ports []corev1.EndpointPort) *corev1.Endpoints {
+	return &corev1.Endpoints{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Endpoints",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:            name,
+			Namespace:       namespace,
+			ResourceVersion: resourceVersion,
+			Labels: map[string]string{
+				"service.kubernetes.io/headless":      "",
+				consts.DefaultExportedServiceSelector: "true",
+			},
+		},
+		Subsets: []corev1.EndpointSubset{
+			{
+				Addresses: []corev1.EndpointAddress{
+					{
+						Hostname: "pod-0",
+						IP:       address,
+						TargetRef: &corev1.ObjectReference{
+							Name:            "pod-0",
+							ResourceVersion: resourceVersion,
+						},
+					},
+					{
+						Hostname: "pod-1",
+						IP:       address,
+						TargetRef: &corev1.ObjectReference{
+							Name:            "pod-1",
+							ResourceVersion: resourceVersion,
+						},
+					},
+				},
+				Ports: ports,
+			},
+		},
+	}
+}
+
 func remoteServiceAsYaml(name, namespace, resourceVersion string, ports []corev1.ServicePort) string {
 	svc := remoteService(name, namespace, resourceVersion, nil, ports)
 
@@ -466,6 +742,25 @@ func remoteServiceAsYaml(name, namespace, resourceVersion string, ports []corev1
 	return string(bytes)
 }
 
+func remoteHeadlessSvcAsYaml(name, namespace, resourceVersion string, ports []corev1.ServicePort) string {
+	svc := remoteHeadlessService(name, namespace, resourceVersion, nil, ports)
+
+	bytes, err := yaml.Marshal(svc)
+	if err != nil {
+		log.Fatal(err)
+	}
+	return string(bytes)
+}
+
+func remoteHeadlessEndpointsAsYaml(name, namespace, resourceVersion, address string, ports []corev1.EndpointPort) string {
+	ep := remoteHeadlessEndpoints(name, namespace, resourceVersion, address, ports)
+
+	bytes, err := yaml.Marshal(ep)
+	if err != nil {
+		log.Fatal(err)
+	}
+	return string(bytes)
+}
 func mirrorService(name, namespace, resourceVersion string, ports []corev1.ServicePort) *corev1.Service {
 	annotations := make(map[string]string)
 	annotations[consts.RemoteResourceVersionAnnotation] = resourceVersion
@@ -491,6 +786,39 @@ func mirrorService(name, namespace, resourceVersion string, ports []corev1.Servi
 	}
 }
 
+func headlessMirrorService(name, namespace, resourceVersion string, ports []corev1.ServicePort) *corev1.Service {
+	svc := mirrorService(name, namespace, resourceVersion, ports)
+	svc.Spec.ClusterIP = "None"
+	return svc
+}
+
+func endpointMirrorService(hostname, rootName, namespace, resourceVersion string, ports []corev1.ServicePort) *corev1.Service {
+	annotations := make(map[string]string)
+	annotations[consts.RemoteResourceVersionAnnotation] = resourceVersion
+	annotations[consts.RemoteServiceFqName] = fmt.Sprintf("%s.%s.%s.svc.cluster.local", hostname, strings.Replace(rootName, "-remote", "", 1), namespace)
+
+	return &corev1.Service{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Service",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      fmt.Sprintf("%s-%s", hostname, clusterName),
+			Namespace: namespace,
+			Labels: map[string]string{
+
+				consts.MirroredHeadlessSvcNameLabel: rootName,
+				consts.RemoteClusterNameLabel:       clusterName,
+				consts.MirroredResourceLabel:        "true",
+			},
+			Annotations: annotations,
+		},
+		Spec: corev1.ServiceSpec{
+			Ports: ports,
+		},
+	}
+}
+
 func mirrorServiceAsYaml(name, namespace, resourceVersion string, ports []corev1.ServicePort) string {
 	svc := mirrorService(name, namespace, resourceVersion, ports)
 
@@ -501,6 +829,27 @@ func mirrorServiceAsYaml(name, namespace, resourceVersion string, ports []corev1
 	return string(bytes)
 }
 
+func headlessMirrorAsYaml(name, namespace, resourceVersion string, ports []corev1.ServicePort) string {
+	svc := headlessMirrorService(name, namespace, resourceVersion, ports)
+
+	bytes, err := yaml.Marshal(svc)
+	if err != nil {
+		log.Fatal(err)
+	}
+	return string(bytes)
+}
+
+func endpointMirrorAsYaml(hostname, rootName, namespace, resourceVersion string, ports []corev1.ServicePort) string {
+	svc := endpointMirrorService(hostname, rootName, namespace, resourceVersion, ports)
+
+	bytes, err := yaml.Marshal(svc)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return string(bytes)
+}
+
 func gateway(name, namespace, resourceVersion, ip, hostname, portName string, port int32, identity string, probePort int32, probePath string, probePeriod int) *corev1.Service {
 	svc := corev1.Service{
 		TypeMeta: metav1.TypeMeta{
@@ -593,6 +942,94 @@ func endpoints(name, namespace, gatewayIP string, gatewayIdentity string, ports
 	return endpoints
 }
 
+func endpointMirrorEndpoints(rootName, namespace, hostname, gatewayIP, gatewayIdentity string, ports []corev1.EndpointPort) *corev1.Endpoints {
+	localName := fmt.Sprintf("%s-%s", hostname, clusterName)
+	ep := endpoints(localName, namespace, gatewayIP, gatewayIdentity, ports)
+
+	ep.Annotations[consts.RemoteServiceFqName] = fmt.Sprintf("%s.%s.%s.svc.cluster.local", hostname, strings.Replace(rootName, "-remote", "", 1), namespace)
+	ep.Labels[consts.MirroredHeadlessSvcNameLabel] = rootName
+
+	return ep
+}
+
+func headlessMirrorEndpoints(name, namespace, hostname, hostIP, gatewayIdentity string, ports []corev1.EndpointPort) *corev1.Endpoints {
+	endpoints := &corev1.Endpoints{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Endpoints",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+			Labels: map[string]string{
+				consts.RemoteClusterNameLabel: clusterName,
+				consts.MirroredResourceLabel:  "true",
+			},
+			Annotations: map[string]string{
+				consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.cluster.local", strings.Replace(name, "-remote", "", 1), namespace),
+			},
+		},
+		Subsets: []corev1.EndpointSubset{
+			{
+				Addresses: []corev1.EndpointAddress{
+					{
+						Hostname: hostname,
+						IP:       hostIP,
+					},
+				},
+				Ports: ports,
+			},
+		},
+	}
+
+	if gatewayIdentity != "" {
+		endpoints.Annotations[consts.RemoteGatewayIdentity] = gatewayIdentity
+	}
+
+	return endpoints
+}
+
+func headlessMirrorEndpointsUpdated(name, namespace string, hostnames, hostIPs []string, gatewayIdentity string, ports []corev1.EndpointPort) *corev1.Endpoints {
+	endpoints := &corev1.Endpoints{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Endpoints",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+			Labels: map[string]string{
+				consts.RemoteClusterNameLabel: clusterName,
+				consts.MirroredResourceLabel:  "true",
+			},
+			Annotations: map[string]string{
+				consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.cluster.local", strings.Replace(name, "-remote", "", 1), namespace),
+			},
+		},
+		Subsets: []corev1.EndpointSubset{
+			{
+				Addresses: []corev1.EndpointAddress{
+					{
+						Hostname: hostnames[0],
+						IP:       hostIPs[0],
+					},
+					{
+						Hostname: hostnames[1],
+						IP:       hostIPs[1],
+					},
+				},
+				Ports: ports,
+			},
+		},
+	}
+
+	if gatewayIdentity != "" {
+		endpoints.Annotations[consts.RemoteGatewayIdentity] = gatewayIdentity
+	}
+
+	return endpoints
+}
+
 func endpointsAsYaml(name, namespace, gatewayIP, gatewayIdentity string, ports []corev1.EndpointPort) string {
 	ep := endpoints(name, namespace, gatewayIP, gatewayIdentity, ports)
 
@@ -602,3 +1039,25 @@ func endpointsAsYaml(name, namespace, gatewayIP, gatewayIdentity string, ports [
 	}
 	return string(bytes)
 }
+
+func headlessMirrorEndpointsAsYaml(name, namespace, hostname, hostIP, gatewayIdentity string, ports []corev1.EndpointPort) string {
+	ep := headlessMirrorEndpoints(name, namespace, hostname, hostIP, gatewayIdentity, ports)
+
+	bytes, err := yaml.Marshal(ep)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return string(bytes)
+}
+
+func endpointMirrorEndpointsAsYaml(name, namespace, hostname, gatewayIP, gatewayIdentity string, ports []corev1.EndpointPort) string {
+	ep := endpointMirrorEndpoints(name, namespace, hostname, gatewayIP, gatewayIdentity, ports)
+
+	bytes, err := yaml.Marshal(ep)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return string(bytes)
+}
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index c186f7a45..9363565d3 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -479,54 +479,6 @@ status:
 
 }
 
-func TestCheckCapability(t *testing.T) {
-	tests := []struct {
-		k8sConfigs []string
-		err        error
-	}{
-		{
-			[]string{},
-			nil,
-		},
-		{
-			[]string{`apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: restricted
-spec:
-  requiredDropCapabilities:
-    - ALL`,
-			},
-			fmt.Errorf("found 1 PodSecurityPolicies, but none provide TEST_CAP, proxy injection will fail if the PSP admission controller is running"),
-		},
-	}
-
-	for i, test := range tests {
-		test := test // pin
-		t.Run(fmt.Sprintf("%d: returns expected capability result", i), func(t *testing.T) {
-			hc := NewHealthChecker(
-				[]CategoryID{},
-				&Options{},
-			)
-
-			var err error
-			hc.kubeAPI, err = k8s.NewFakeAPI(test.k8sConfigs...)
-			if err != nil {
-				t.Fatalf("Unexpected error: %s", err)
-			}
-
-			err = hc.checkCapability(context.Background(), "TEST_CAP")
-			if err != nil || test.err != nil {
-				if (err == nil && test.err != nil) ||
-					(err != nil && test.err == nil) ||
-					(err.Error() != test.err.Error()) {
-					t.Fatalf("Unexpected error (Expected: %s, Got: %s)", test.err, err)
-				}
-			}
-		})
-	}
-}
-
 func TestConfigExists(t *testing.T) {
 	testCases := []struct {
 		k8sConfigs []string
@@ -993,7 +945,6 @@ metadata:
 				"linkerd-config control plane CustomResourceDefinitions exist",
 				"linkerd-config control plane MutatingWebhookConfigurations exist",
 				"linkerd-config control plane ValidatingWebhookConfigurations exist",
-				"linkerd-config control plane PodSecurityPolicies exist: missing PodSecurityPolicies: linkerd-test-ns-control-plane",
 			},
 		},
 		{
@@ -1103,14 +1054,6 @@ metadata:
   name: linkerd-sp-validator-webhook-config
   labels:
     linkerd.io/control-plane-ns: test-ns
-`,
-				`
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-test-ns-control-plane
-  labels:
-    linkerd.io/control-plane-ns: test-ns
 `,
 			},
 			[]string{
@@ -1121,7 +1064,6 @@ metadata:
 				"linkerd-config control plane CustomResourceDefinitions exist",
 				"linkerd-config control plane MutatingWebhookConfigurations exist",
 				"linkerd-config control plane ValidatingWebhookConfigurations exist",
-				"linkerd-config control plane PodSecurityPolicies exist",
 			},
 		},
 	}
@@ -2046,7 +1988,6 @@ func TestLinkerdPreInstallGlobalResourcesChecks(t *testing.T) {
 			"pre-linkerd-global-resources no CustomResourceDefinitions exist",
 			"pre-linkerd-global-resources no MutatingWebhookConfigurations exist",
 			"pre-linkerd-global-resources no ValidatingWebhookConfigurations exist",
-			"pre-linkerd-global-resources no PodSecurityPolicies exist",
 		}
 		if !reflect.DeepEqual(observer.results, expected) {
 			testutil.AnnotatedErrorf(t, "Mismatch result", "Mismatch result.\nExpected: %v\n Actual: %v\n", expected, observer.results)
@@ -2083,12 +2024,6 @@ metadata:
 kind: ValidatingWebhookConfiguration
 metadata:
   name: validating-webhook-configuration
-  labels:
-    linkerd.io/control-plane-ns: test-ns`,
-			`apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: pod-security-policy
   labels:
     linkerd.io/control-plane-ns: test-ns`,
 		}
@@ -2111,7 +2046,6 @@ metadata:
 			"pre-linkerd-global-resources no CustomResourceDefinitions exist: CustomResourceDefinitions found but should not exist: custom-resource-definition",
 			"pre-linkerd-global-resources no MutatingWebhookConfigurations exist: MutatingWebhookConfigurations found but should not exist: mutating-webhook-configuration",
 			"pre-linkerd-global-resources no ValidatingWebhookConfigurations exist: ValidatingWebhookConfigurations found but should not exist: validating-webhook-configuration",
-			"pre-linkerd-global-resources no PodSecurityPolicies exist: PodSecurityPolicies found but should not exist: pod-security-policy",
 		}
 		if !reflect.DeepEqual(observer.results, expected) {
 			t.Errorf("Mismatch result.\nExpected: %v\n Actual: %v\n", expected, observer.results)
@@ -2984,11 +2918,8 @@ func TestLinkerdIdentityCheckCertValidity(t *testing.T) {
 
 type fakeCniResourcesOpts struct {
 	hasConfigMap          bool
-	hasPodSecurityPolicy  bool
 	hasClusterRole        bool
 	hasClusterRoleBinding bool
-	hasRole               bool
-	hasRoleBinding        bool
 	hasServiceAccount     bool
 	hasDaemonSet          bool
 	scheduled             int
@@ -3013,32 +2944,6 @@ data:
 `)
 	}
 
-	if opts.hasPodSecurityPolicy {
-		resources = append(resources, `
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: linkerd-test-ns-cni
-  labels:
-    linkerd.io/cni-resource: "true"
-spec:
-  allowPrivilegeEscalation: false
-  fsGroup:
-    rule: RunAsAny
-  hostNetwork: true
-  runAsUser:
-    rule: RunAsAny
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  volumes:
-  - hostPath
-  - secret
----
-`)
-	}
-
 	if opts.hasClusterRole {
 		resources = append(resources, `
 kind: ClusterRole
@@ -3075,46 +2980,6 @@ subjects:
 `)
 	}
 
-	if opts.hasRole {
-		resources = append(resources, `
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: linkerd-cni
-  namespace: test-ns
-  labels:
-    linkerd.io/cni-resource: "true"
-rules:
-- apiGroups: ['extensions', 'policy']
-  resources: ['podsecuritypolicies']
-  resourceNames:
-  - linkerd-test-ns-cni
-  verbs: ['use']
----
-`)
-	}
-
-	if opts.hasRoleBinding {
-		resources = append(resources, `
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: linkerd-cni
-  namespace: test-ns
-  labels:
-    linkerd.io/cni-resource: "true"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: linkerd-cni
-subjects:
-- kind: ServiceAccount
-  name: linkerd-cni
-  namespace: test-ns
----
-`)
-	}
-
 	if opts.hasServiceAccount {
 		resources = append(resources, `
 apiVersion: v1
@@ -3218,88 +3083,49 @@ func TestCniChecks(t *testing.T) {
 			fakeCniResourcesOpts{},
 			[]string{"linkerd-cni-plugin cni plugin ConfigMap exists: configmaps \"linkerd-cni-config\" not found"},
 		},
-		{
-			"fails when there is no pod security policy",
-			fakeCniResourcesOpts{hasConfigMap: true},
-			[]string{
-				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists: missing PodSecurityPolicy: linkerd-test-ns-cni"},
-		},
 		{
 			"fails then there is no ClusterRole",
-			fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true},
+			fakeCniResourcesOpts{hasConfigMap: true},
 			[]string{
 				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists",
 				"linkerd-cni-plugin cni plugin ClusterRole exists: missing ClusterRole: linkerd-cni"},
 		},
 		{
 			"fails then there is no ClusterRoleBinding",
-			fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true},
+			fakeCniResourcesOpts{hasConfigMap: true, hasClusterRole: true},
 			[]string{
 				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists",
 				"linkerd-cni-plugin cni plugin ClusterRole exists",
 				"linkerd-cni-plugin cni plugin ClusterRoleBinding exists: missing ClusterRoleBinding: linkerd-cni"},
 		},
-		{
-			"fails then there is no Role",
-			fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true},
-			[]string{
-				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists",
-				"linkerd-cni-plugin cni plugin ClusterRole exists",
-				"linkerd-cni-plugin cni plugin ClusterRoleBinding exists",
-				"linkerd-cni-plugin cni plugin Role exists: missing Role: linkerd-cni"},
-		},
-		{
-			"fails then there is no RoleBinding",
-			fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true},
-			[]string{
-				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists",
-				"linkerd-cni-plugin cni plugin ClusterRole exists",
-				"linkerd-cni-plugin cni plugin ClusterRoleBinding exists",
-				"linkerd-cni-plugin cni plugin Role exists",
-				"linkerd-cni-plugin cni plugin RoleBinding exists: missing RoleBinding: linkerd-cni"},
-		},
 		{
 			"fails then there is no ServiceAccount",
-			fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true, hasRoleBinding: true},
+			fakeCniResourcesOpts{hasConfigMap: true, hasClusterRole: true, hasClusterRoleBinding: true},
 			[]string{
 				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists",
 				"linkerd-cni-plugin cni plugin ClusterRole exists",
 				"linkerd-cni-plugin cni plugin ClusterRoleBinding exists",
-				"linkerd-cni-plugin cni plugin Role exists",
-				"linkerd-cni-plugin cni plugin RoleBinding exists",
 				"linkerd-cni-plugin cni plugin ServiceAccount exists: missing ServiceAccount: linkerd-cni",
 			},
 		},
 		{
 			"fails then there is no DaemonSet",
-			fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true, hasRoleBinding: true, hasServiceAccount: true},
+			fakeCniResourcesOpts{hasConfigMap: true, hasClusterRole: true, hasClusterRoleBinding: true, hasServiceAccount: true},
 			[]string{
 				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists",
 				"linkerd-cni-plugin cni plugin ClusterRole exists",
 				"linkerd-cni-plugin cni plugin ClusterRoleBinding exists",
-				"linkerd-cni-plugin cni plugin Role exists",
-				"linkerd-cni-plugin cni plugin RoleBinding exists",
 				"linkerd-cni-plugin cni plugin ServiceAccount exists",
 				"linkerd-cni-plugin cni plugin DaemonSet exists: missing DaemonSet: linkerd-cni",
 			},
 		},
 		{
 			"fails then there is nodes are not ready",
-			fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true, hasRoleBinding: true, hasServiceAccount: true, hasDaemonSet: true, scheduled: 5, ready: 4},
+			fakeCniResourcesOpts{hasConfigMap: true, hasClusterRole: true, hasClusterRoleBinding: true, hasServiceAccount: true, hasDaemonSet: true, scheduled: 5, ready: 4},
 			[]string{
 				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists",
 				"linkerd-cni-plugin cni plugin ClusterRole exists",
 				"linkerd-cni-plugin cni plugin ClusterRoleBinding exists",
-				"linkerd-cni-plugin cni plugin Role exists",
-				"linkerd-cni-plugin cni plugin RoleBinding exists",
 				"linkerd-cni-plugin cni plugin ServiceAccount exists",
 				"linkerd-cni-plugin cni plugin DaemonSet exists",
 				"linkerd-cni-plugin cni plugin pod is running on all nodes: number ready: 4, number scheduled: 5",
@@ -3307,14 +3133,11 @@ func TestCniChecks(t *testing.T) {
 		},
 		{
 			"fails then there is nodes are not ready",
-			fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true, hasRoleBinding: true, hasServiceAccount: true, hasDaemonSet: true, scheduled: 5, ready: 5},
+			fakeCniResourcesOpts{hasConfigMap: true, hasClusterRole: true, hasClusterRoleBinding: true, hasServiceAccount: true, hasDaemonSet: true, scheduled: 5, ready: 5},
 			[]string{
 				"linkerd-cni-plugin cni plugin ConfigMap exists",
-				"linkerd-cni-plugin cni plugin PodSecurityPolicy exists",
 				"linkerd-cni-plugin cni plugin ClusterRole exists",
 				"linkerd-cni-plugin cni plugin ClusterRoleBinding exists",
-				"linkerd-cni-plugin cni plugin Role exists",
-				"linkerd-cni-plugin cni plugin RoleBinding exists",
 				"linkerd-cni-plugin cni plugin ServiceAccount exists",
 				"linkerd-cni-plugin cni plugin DaemonSet exists",
 				"linkerd-cni-plugin cni plugin pod is running on all nodes",
diff --git a/test/integration/serviceaccounts/serviceaccounts_test.go b/test/integration/serviceaccounts/serviceaccounts_test.go
index 58f46bace..d65b5e909 100644
--- a/test/integration/serviceaccounts/serviceaccounts_test.go
+++ b/test/integration/serviceaccounts/serviceaccounts_test.go
@@ -55,21 +55,4 @@ func TestServiceAccountsMatch(t *testing.T) {
 	if len(saNames) < len(expectedNames) || !namesMatch(saNames) {
 		testutil.Fatalf(t, "the service account list doesn't match the expected list: %s", expectedNames)
 	}
-
-	res, err = TestHelper.Kubectl("",
-		"-n", TestHelper.GetLinkerdNamespace(),
-		"get", "rolebindings", "linkerd-psp",
-		"--output", "jsonpath={.subjects[*].name}",
-	)
-	if err != nil {
-		testutil.AnnotatedFatalf(t, "error retrieving list of linkerd-psp rolebindings",
-			"error retrieving list of linkerd-psp rolebindings: %s", err)
-	}
-	saNamesPSP := strings.Split(res, " ")
-
-	if len(saNamesPSP) < len(expectedNames) || !namesMatch(saNamesPSP) {
-		t.Fatalf(
-			"The service accounts in the linkerd-psp rolebindings don't match the expected list: %s",
-			expectedNames)
-	}
 }
diff --git a/test/integration/testdata/check.cni.golden b/test/integration/testdata/check.cni.golden
index bcd102a9a..17a79724f 100644
--- a/test/integration/testdata/check.cni.golden
+++ b/test/integration/testdata/check.cni.golden
@@ -29,16 +29,12 @@ linkerd-config
 √ control plane CustomResourceDefinitions exist
 √ control plane MutatingWebhookConfigurations exist
 √ control plane ValidatingWebhookConfigurations exist
-√ control plane PodSecurityPolicies exist
 
 linkerd-cni-plugin
 ------------------
 √ cni plugin ConfigMap exists
-√ cni plugin PodSecurityPolicy exists
 √ cni plugin ClusterRole exists
 √ cni plugin ClusterRoleBinding exists
-√ cni plugin Role exists
-√ cni plugin RoleBinding exists
 √ cni plugin ServiceAccount exists
 √ cni plugin DaemonSet exists
 √ cni plugin pod is running on all nodes
diff --git a/test/integration/testdata/check.cni.proxy.golden b/test/integration/testdata/check.cni.proxy.golden
index 185b2c44a..14b84776e 100644
--- a/test/integration/testdata/check.cni.proxy.golden
+++ b/test/integration/testdata/check.cni.proxy.golden
@@ -29,16 +29,12 @@ linkerd-config
 √ control plane CustomResourceDefinitions exist
 √ control plane MutatingWebhookConfigurations exist
 √ control plane ValidatingWebhookConfigurations exist
-√ control plane PodSecurityPolicies exist
 
 linkerd-cni-plugin
 ------------------
 √ cni plugin ConfigMap exists
-√ cni plugin PodSecurityPolicy exists
 √ cni plugin ClusterRole exists
 √ cni plugin ClusterRoleBinding exists
-√ cni plugin Role exists
-√ cni plugin RoleBinding exists
 √ cni plugin ServiceAccount exists
 √ cni plugin DaemonSet exists
 √ cni plugin pod is running on all nodes
diff --git a/test/integration/testdata/check.config.golden b/test/integration/testdata/check.config.golden
index c53f0c34d..c04d6220e 100644
--- a/test/integration/testdata/check.config.golden
+++ b/test/integration/testdata/check.config.golden
@@ -20,7 +20,6 @@ linkerd-config
 √ control plane CustomResourceDefinitions exist
 √ control plane MutatingWebhookConfigurations exist
 √ control plane ValidatingWebhookConfigurations exist
-√ control plane PodSecurityPolicies exist
 
 linkerd-version
 ---------------
diff --git a/test/integration/testdata/check.golden b/test/integration/testdata/check.golden
index 44dd0e3f9..33efc8a3c 100644
--- a/test/integration/testdata/check.golden
+++ b/test/integration/testdata/check.golden
@@ -29,7 +29,6 @@ linkerd-config
 √ control plane CustomResourceDefinitions exist
 √ control plane MutatingWebhookConfigurations exist
 √ control plane ValidatingWebhookConfigurations exist
-√ control plane PodSecurityPolicies exist
 
 linkerd-identity
 ----------------
diff --git a/test/integration/testdata/check.multicluster.proxy.golden b/test/integration/testdata/check.multicluster.proxy.golden
index 9e512eba2..4bf5606c4 100644
--- a/test/integration/testdata/check.multicluster.proxy.golden
+++ b/test/integration/testdata/check.multicluster.proxy.golden
@@ -29,7 +29,6 @@ linkerd-config
 √ control plane CustomResourceDefinitions exist
 √ control plane MutatingWebhookConfigurations exist
 √ control plane ValidatingWebhookConfigurations exist
-√ control plane PodSecurityPolicies exist
 
 linkerd-identity
 ----------------
diff --git a/test/integration/testdata/check.pre.golden b/test/integration/testdata/check.pre.golden
index fd02fae33..1f72a63c9 100644
--- a/test/integration/testdata/check.pre.golden
+++ b/test/integration/testdata/check.pre.golden
@@ -25,11 +25,6 @@ pre-kubernetes-setup
 √ can read extension-apiserver-authentication configmap
 √ no clock skew detected
 
-pre-kubernetes-capability
--------------------------
-√ has NET_ADMIN capability
-√ has NET_RAW capability
-
 linkerd-version
 ---------------
 √ can determine the latest version
diff --git a/test/integration/testdata/check.proxy.golden b/test/integration/testdata/check.proxy.golden
index 58ab20b93..96c6e7c58 100644
--- a/test/integration/testdata/check.proxy.golden
+++ b/test/integration/testdata/check.proxy.golden
@@ -29,7 +29,6 @@ linkerd-config
 √ control plane CustomResourceDefinitions exist
 √ control plane MutatingWebhookConfigurations exist
 √ control plane ValidatingWebhookConfigurations exist
-√ control plane PodSecurityPolicies exist
 
 linkerd-identity
 ----------------
diff --git a/test/integration/uninstall/testdata/check.pre.golden b/test/integration/uninstall/testdata/check.pre.golden
index fd02fae33..1f72a63c9 100644
--- a/test/integration/uninstall/testdata/check.pre.golden
+++ b/test/integration/uninstall/testdata/check.pre.golden
@@ -25,11 +25,6 @@ pre-kubernetes-setup
 √ can read extension-apiserver-authentication configmap
 √ no clock skew detected
 
-pre-kubernetes-capability
--------------------------
-√ has NET_ADMIN capability
-√ has NET_RAW capability
-
 linkerd-version
 ---------------
 √ can determine the latest version
diff --git a/viz/cmd/testdata/install_default.golden b/viz/cmd/testdata/install_default.golden
index 964dceace..c6e0a4b8d 100644
--- a/viz/cmd/testdata/install_default.golden
+++ b/viz/cmd/testdata/install_default.golden
@@ -309,9 +309,6 @@ rules:
 - apiGroups: ["admissionregistration.k8s.io"]
   resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"]
   verbs: ["list"]
-- apiGroups: ["policy"]
-  resources: ["podsecuritypolicies"]
-  verbs: ["list"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
@@ -392,52 +389,6 @@ metadata:
     component: web
     namespace: linkerd-viz
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: viz-psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-    namespace: linkerd-viz
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: tap
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: web
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: grafana
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: prometheus
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: metrics-api
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: tap-injector
-  namespace: linkerd-viz
----
 ###
 ### Metrics API
 ###
diff --git a/viz/cmd/testdata/install_default_overrides.golden b/viz/cmd/testdata/install_default_overrides.golden
index 1fbec3b4a..ca6c3406f 100644
--- a/viz/cmd/testdata/install_default_overrides.golden
+++ b/viz/cmd/testdata/install_default_overrides.golden
@@ -309,9 +309,6 @@ rules:
 - apiGroups: ["admissionregistration.k8s.io"]
   resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"]
   verbs: ["list"]
-- apiGroups: ["policy"]
-  resources: ["podsecuritypolicies"]
-  verbs: ["list"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
@@ -392,52 +389,6 @@ metadata:
     component: web
     namespace: linkerd-viz
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: viz-psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-    namespace: linkerd-viz
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: tap
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: web
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: grafana
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: prometheus
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: metrics-api
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: tap-injector
-  namespace: linkerd-viz
----
 ###
 ### Metrics API
 ###
diff --git a/viz/cmd/testdata/install_grafana_disabled.golden b/viz/cmd/testdata/install_grafana_disabled.golden
index e0e58c12c..65d2f4fa7 100644
--- a/viz/cmd/testdata/install_grafana_disabled.golden
+++ b/viz/cmd/testdata/install_grafana_disabled.golden
@@ -296,9 +296,6 @@ rules:
 - apiGroups: ["admissionregistration.k8s.io"]
   resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"]
   verbs: ["list"]
-- apiGroups: ["policy"]
-  resources: ["podsecuritypolicies"]
-  verbs: ["list"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
@@ -379,49 +376,6 @@ metadata:
     component: web
     namespace: linkerd-viz
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: viz-psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-    namespace: linkerd-viz
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: tap
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: web
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: prometheus
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: metrics-api
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: tap-injector
-  namespace: linkerd-viz
----
 ###
 ### Metrics API
 ###
diff --git a/viz/cmd/testdata/install_prometheus_disabled.golden b/viz/cmd/testdata/install_prometheus_disabled.golden
index fcf6291e3..de63b000a 100644
--- a/viz/cmd/testdata/install_prometheus_disabled.golden
+++ b/viz/cmd/testdata/install_prometheus_disabled.golden
@@ -269,9 +269,6 @@ rules:
 - apiGroups: ["admissionregistration.k8s.io"]
   resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"]
   verbs: ["list"]
-- apiGroups: ["policy"]
-  resources: ["podsecuritypolicies"]
-  verbs: ["list"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
@@ -352,49 +349,6 @@ metadata:
     component: web
     namespace: linkerd-viz
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: viz-psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-    namespace: linkerd-viz
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: tap
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: web
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: grafana
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: metrics-api
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: tap-injector
-  namespace: linkerd-viz
----
 ###
 ### Metrics API
 ###
diff --git a/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden b/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
index 2cc2db523..36f792164 100644
--- a/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
+++ b/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
@@ -309,9 +309,6 @@ rules:
 - apiGroups: ["admissionregistration.k8s.io"]
   resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"]
   verbs: ["list"]
-- apiGroups: ["policy"]
-  resources: ["podsecuritypolicies"]
-  verbs: ["list"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
@@ -392,52 +389,6 @@ metadata:
     component: web
     namespace: linkerd-viz
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: viz-psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-    namespace: linkerd-viz
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: tap
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: web
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: grafana
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: prometheus
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: metrics-api
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: tap-injector
-  namespace: linkerd-viz
----
 ###
 ### Metrics API
 ###
diff --git a/viz/cmd/testdata/install_proxy_resources.golden b/viz/cmd/testdata/install_proxy_resources.golden
index b024e4bcc..b3afea7ce 100644
--- a/viz/cmd/testdata/install_proxy_resources.golden
+++ b/viz/cmd/testdata/install_proxy_resources.golden
@@ -309,9 +309,6 @@ rules:
 - apiGroups: ["admissionregistration.k8s.io"]
   resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"]
   verbs: ["list"]
-- apiGroups: ["policy"]
-  resources: ["podsecuritypolicies"]
-  verbs: ["list"]
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
@@ -392,52 +389,6 @@ metadata:
     component: web
     namespace: linkerd-viz
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-rules:
-- apiGroups: ['policy', 'extensions']
-  resources: ['podsecuritypolicies']
-  verbs: ['use']
-  resourceNames:
-  - linkerd-linkerd-control-plane
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: viz-psp
-  namespace: linkerd-viz
-  labels:
-    linkerd.io/extension: viz
-    namespace: linkerd-viz
-roleRef:
-  kind: Role
-  name: psp
-  apiGroup: rbac.authorization.k8s.io
-subjects:
-- kind: ServiceAccount
-  name: tap
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: web
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: grafana
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: prometheus
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: metrics-api
-  namespace: linkerd-viz
-- kind: ServiceAccount
-  name: tap-injector
-  namespace: linkerd-viz
----
 ###
 ### Metrics API
 ###