Repository: linkerd/linkerd2
Pull request #8152 (2022-03-27 17:02:11)

build(deps): bump @fortawesome/fontawesome-svg-core from 6.1.0 to 6.1.1 in /web/app
Bumps [@fortawesome/fontawesome-svg-core](https://github.com/FortAwesome/Font-Awesome) from 6.1.0 to 6.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/FortAwesome/Font-Awesome/releases"><code>@​fortawesome/fontawesome-svg-core</code>'s releases</a>.</em></p>
<blockquote>
<h2>Release 6.1.1</h2>
<p><strong>Change log available at <a href="https://fontawesome.com/docs/changelog/">https://fontawesome.com/docs/changelog/</a></strong></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/FortAwesome/Font-Awesome/commit/28e297f07af26f148c15e6cbbd12cea3027371d3"><code>28e297f</code></a> Release 6.1.1 (<a href="https://github-redirect.dependabot.com/FortAwesome/Font-Awesome/issues/18843">#18843</a>)</li>
<li>See full diff in <a href="https://github.com/FortAwesome/Font-Awesome/compare/6.1.0...6.1.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@fortawesome/fontawesome-svg-core&package-manager=npm_and_yarn&previous-version=6.1.0&new-version=6.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

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


</details>


Patch:
diff --git a/.github/workflows/policy_controller.yml b/.github/workflows/policy_controller.yml
index dba50227f..d7898c3f5 100644
--- a/.github/workflows/policy_controller.yml
+++ b/.github/workflows/policy_controller.yml
@@ -131,6 +131,7 @@ jobs:
     strategy:
       matrix:
         component:
+          - cli-bin
           - controller
           - policy-controller
           - proxy
@@ -162,6 +163,8 @@ jobs:
     name: Policy controller integration
     runs-on: ubuntu-20.04
     timeout-minutes: 20
+    env:
+      LINKERD_DOCKER_REGISTRY: ghcr.io/linkerd
     steps:
       - name: Checkout code
         uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
@@ -195,14 +198,21 @@ jobs:
           path: image-archives
       - name: Load images
         run: |
+          docker pull -q docker.io/bitnami/kubectl:latest &
+          docker pull -q docker.io/curlimages/curl:latest &
+          docker pull -q docker.io/library/nginx:latest &
+          docker pull -q ghcr.io/linkerd/proxy-init:$PROXY_INIT_VERSION &
+          wait # Wait for pulls to complete
           docker load <image-archives/controller.tar
           docker load <image-archives/policy-controller.tar
           docker load <image-archives/proxy.tar
-          docker pull ghcr.io/linkerd/proxy-init:$PROXY_INIT_VERSION
-          docker image ls | grep ghcr.io/linkerd
+          docker image ls | grep -F -e bitnami/kubectl -e curlimages/curl -e nginx -e ghcr.io/linkerd
           tag="$(CI_FORCE_CLEAN=1 bin/root-tag)"
           # Image loading is flakey in CI, so retry!
           until k3d image import \
+                  docker.io/bitnami/kubectl:latest \
+                  docker.io/curlimages/curl:latest \
+                  docker.io/library/nginx:latest \
                   ghcr.io/linkerd/controller:$tag \
                   ghcr.io/linkerd/policy-controller:$tag \
                   ghcr.io/linkerd/proxy:$tag \
@@ -211,17 +221,17 @@ jobs:
             echo retrying...
           done
 
-      # Build the CLI and install Linkerd.
-      - uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab
-        with:
-          go-version: '1.17'
-      - name: Build the Linkerd CLI
-        run: bin/linkerd version --short --client
-      - run: bin/linkerd check --pre --wait=1m
-      - run: bin/linkerd install | kubectl apply -f -
-        env:
-          LINKERD_DOCKER_REGISTRY: ghcr.io/linkerd
-      - run: bin/linkerd check --wait=1m
+      # Install the Linkerd CLI from a docker image
+      - name: Install the Linkerd CLI
+        run: |
+          tag="$(CI_FORCE_CLEAN=1 bin/root-tag)"
+          docker load <image-archives/cli-bin.tar
+          container_id=$(docker create "ghcr.io/linkerd/cli-bin:$tag")
+          docker cp "$container_id":/out/linkerd-linux-amd64 /usr/local/bin/linkerd
+          docker rm "$container_id"
+      - run: linkerd check --pre --wait=1m
+      - run: linkerd install | kubectl apply -f -
+      - run: linkerd check --wait=1m
 
       # Run the tests.
       - run: cargo test -p linkerd-policy-test --frozen
diff --git a/.proxy-version b/.proxy-version
index f1d6feca9..a5ba8a67e 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.174.0
+v2.175.0
diff --git a/Cargo.lock b/Cargo.lock
index 2b167a8d1..a89b4ac04 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -60,9 +60,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.52"
+version = "0.1.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
+checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -974,6 +974,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_yaml",
+ "thiserror",
  "tokio",
  "tracing",
 ]
@@ -1006,6 +1007,7 @@ dependencies = [
  "ipnet",
  "k8s-openapi",
  "kube",
+ "linkerd-policy-controller-core",
  "linkerd-policy-controller-k8s-api",
  "linkerd2-proxy-api",
  "maplit",
@@ -1110,9 +1112,9 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
 
 [[package]]
 name = "native-tls"
-version = "0.2.8"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
+checksum = "09bf6f32a3afefd0b587ee42ed19acd945c6d1f3b5424040f50b2f24ab16be77"
 dependencies = [
  "lazy_static",
  "libc",
@@ -1401,9 +1403,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.16"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57"
+checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58"
 dependencies = [
  "proc-macro2",
 ]
@@ -1440,9 +1442,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.2.11"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c"
+checksum = "8ae183fc1b06c149f0c1793e1eb447c8b04bfe46d48e9e48bfb8d2d7ed64ecf0"
 dependencies = [
  "bitflags",
 ]
diff --git a/charts/linkerd-crds/templates/policy/serverauthorization.yaml b/charts/linkerd-crds/templates/policy/server-authorization.yaml
similarity index 92%
rename from charts/linkerd-crds/templates/policy/serverauthorization.yaml
rename to charts/linkerd-crds/templates/policy/server-authorization.yaml
index 3474f3999..cd5aa0933 100644
--- a/charts/linkerd-crds/templates/policy/serverauthorization.yaml
+++ b/charts/linkerd-crds/templates/policy/server-authorization.yaml
@@ -52,9 +52,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -77,9 +74,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -104,10 +98,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -182,9 +172,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -207,9 +194,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -234,10 +218,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/install.go b/cli/cmd/install.go
index e3aa93450..ae88fcff2 100644
--- a/cli/cmd/install.go
+++ b/cli/cmd/install.go
@@ -62,7 +62,7 @@ Otherwise, you can use the --ignore-cluster flag to overwrite the existing globa
 var (
 	templatesCrdFiles = []string{
 		"templates/policy/server.yaml",
-		"templates/policy/serverauthorization.yaml",
+		"templates/policy/server-authorization.yaml",
 		"templates/serviceprofile.yaml",
 	}
 
diff --git a/policy-controller/k8s/api/Cargo.toml b/policy-controller/k8s/api/Cargo.toml
index af6424785..aecfb9d64 100644
--- a/policy-controller/k8s/api/Cargo.toml
+++ b/policy-controller/k8s/api/Cargo.toml
@@ -13,5 +13,6 @@ schemars = "0.8"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 serde_yaml = "0.8"
+thiserror = "1"
 tokio = { version = "1", features = ["time"] }
 tracing = "0.1"
diff --git a/policy-controller/k8s/api/src/policy.rs b/policy-controller/k8s/api/src/policy.rs
index 95ed40844..a3364281d 100644
--- a/policy-controller/k8s/api/src/policy.rs
+++ b/policy-controller/k8s/api/src/policy.rs
@@ -1,5 +1,9 @@
+pub mod network;
 pub mod server;
 pub mod server_authorization;
 
-pub use self::server::{Server, ServerSpec};
-pub use self::server_authorization::{ServerAuthorization, ServerAuthorizationSpec};
+pub use self::{
+    network::Network,
+    server::{Server, ServerSpec},
+    server_authorization::{ServerAuthorization, ServerAuthorizationSpec},
+};
diff --git a/policy-controller/k8s/api/src/policy/network.rs b/policy-controller/k8s/api/src/policy/network.rs
new file mode 100644
index 000000000..07a915c21
--- /dev/null
+++ b/policy-controller/k8s/api/src/policy/network.rs
@@ -0,0 +1,103 @@
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct Network {
+    pub cidr: Cidr,
+    pub except: Option<Vec<Cidr>>,
+}
+
+#[derive(
+    Copy, Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
+)]
+#[serde(untagged)]
+pub enum Cidr {
+    Addr(std::net::IpAddr),
+    Net(ipnet::IpNet),
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("not a valid CIDR or IP address: {0}")]
+pub struct CidrParseError(String);
+
+// === impl Cidr ===
+
+impl Cidr {
+    #[inline]
+    pub fn contains(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Self::Net(this), Self::Net(other)) => this.contains(other),
+            (Self::Net(this), Self::Addr(other)) => this.contains(other),
+            (Self::Addr(this), Self::Net(other)) => ipnet::IpNet::from(*this).contains(other),
+            (Self::Addr(this), Self::Addr(other)) => this == other,
+        }
+    }
+}
+
+impl std::str::FromStr for Cidr {
+    type Err = CidrParseError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Ok(net) = s.parse() {
+            return Ok(Self::Net(net));
+        }
+
+        if let Ok(addr) = s.parse() {
+            return Ok(Self::Addr(addr));
+        }
+
+        Err(CidrParseError(s.to_string()))
+    }
+}
+
+impl From<Cidr> for ipnet::IpNet {
+    fn from(cidr: Cidr) -> ipnet::IpNet {
+        match cidr {
+            Cidr::Net(net) => net,
+            Cidr::Addr(addr) => ipnet::IpNet::from(addr),
+        }
+    }
+}
+
+impl From<ipnet::IpNet> for Cidr {
+    fn from(net: ipnet::IpNet) -> Self {
+        Self::Net(net)
+    }
+}
+
+impl From<ipnet::Ipv4Net> for Cidr {
+    fn from(net: ipnet::Ipv4Net) -> Self {
+        Self::Net(net.into())
+    }
+}
+
+impl From<ipnet::Ipv6Net> for Cidr {
+    fn from(net: ipnet::Ipv6Net) -> Self {
+        Self::Net(net.into())
+    }
+}
+
+impl From<std::net::IpAddr> for Cidr {
+    fn from(net: std::net::IpAddr) -> Self {
+        Self::Addr(net)
+    }
+}
+
+impl From<std::net::Ipv4Addr> for Cidr {
+    fn from(addr: std::net::Ipv4Addr) -> Self {
+        Self::Addr(addr.into())
+    }
+}
+
+impl From<std::net::Ipv6Addr> for Cidr {
+    fn from(addr: std::net::Ipv6Addr) -> Self {
+        Self::Addr(addr.into())
+    }
+}
+
+impl std::fmt::Display for Cidr {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Addr(addr) => addr.fmt(f),
+            Self::Net(net) => net.fmt(f),
+        }
+    }
+}
diff --git a/policy-controller/k8s/api/src/policy/server_authorization.rs b/policy-controller/k8s/api/src/policy/server_authorization.rs
index f74c43cc5..c8a049dd7 100644
--- a/policy-controller/k8s/api/src/policy/server_authorization.rs
+++ b/policy-controller/k8s/api/src/policy/server_authorization.rs
@@ -1,5 +1,5 @@
-use super::super::labels;
-use ipnet::IpNet;
+pub use super::Network;
+use crate::labels;
 use kube::CustomResource;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -55,12 +55,6 @@ pub struct MeshTls {
     pub service_accounts: Option<Vec<ServiceAccountRef>>,
 }
 
-#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
-pub struct Network {
-    pub cidr: IpNet,
-    pub except: Option<Vec<IpNet>>,
-}
-
 /// References a Kubernetes `ServiceAccount` instance.
 ///
 /// If no namespace is specified, the `Authorization`'s namespace is used.
diff --git a/policy-controller/k8s/index/src/server_authorization.rs b/policy-controller/k8s/index/src/server_authorization.rs
index 7609556ec..ce7b6b163 100644
--- a/policy-controller/k8s/index/src/server_authorization.rs
+++ b/policy-controller/k8s/index/src/server_authorization.rs
@@ -69,8 +69,8 @@ fn client_authz(
         .into_iter()
         .flatten()
         .map(|net| NetworkMatch {
-            net: net.cidr,
-            except: net.except.unwrap_or_default(),
+            net: net.cidr.into(),
+            except: net.except.into_iter().flatten().map(Into::into).collect(),
         })
         .collect();
 
diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs
index 062570823..a44c92345 100644
--- a/policy-controller/src/admission.rs
+++ b/policy-controller/src/admission.rs
@@ -9,7 +9,7 @@ use kube::{core::DynamicObject, Resource, ResourceExt};
 use serde::de::DeserializeOwned;
 use std::task;
 use thiserror::Error;
-use tracing::{debug, info, warn};
+use tracing::{debug, info, trace, warn};
 
 #[derive(Clone)]
 pub struct Admission {
@@ -47,7 +47,7 @@ impl hyper::service::Service<Request<Body>> for Admission {
     }
 
     fn call(&mut self, req: Request<Body>) -> Self::Future {
-        tracing::trace!(?req);
+        trace!(?req);
         if req.method() != http::Method::POST || req.uri().path() != "/" {
             return Box::pin(future::ok(
                 Response::builder()
@@ -67,7 +67,7 @@ impl hyper::service::Service<Request<Body>> for Admission {
                     return json_response(AdmissionResponse::invalid(error).into_review());
                 }
             };
-            tracing::trace!(?review);
+            trace!(?review);
 
             let rsp = match review.try_into() {
                 Ok(req) => {
@@ -110,18 +110,19 @@ impl Admission {
         T: DeserializeOwned,
         Self: Validate<T>,
     {
-        let kind = req.kind.kind.clone();
         let rsp = AdmissionResponse::from(&req);
+
+        let kind = req.kind.kind.clone();
         let (ns, name, spec) = match parse_spec::<T>(req) {
             Ok(spec) => spec,
             Err(error) => {
-                info!(%error, "failed to parse {} spec", kind);
+                info!(%error, "Failed to parse {} spec", kind);
                 return rsp.deny(error);
             }
         };
 
         if let Err(error) = self.validate(&ns, &name, spec).await {
-            info!(%error, %ns, %name, %kind, "denied");
+            info!(%error, %ns, %name, %kind, "Denied");
             return rsp.deny(error);
         }
 
@@ -138,20 +139,6 @@ where
     *req.kind.group == *T::group(&dt) && *req.kind.kind == *T::kind(&dt)
 }
 
-/// Detects whether two pod selectors can select the same pod
-//
-// TODO(ver) We can probably detect overlapping selectors more effectively. For
-// example, if `left` selects pods with 'foo=bar' and `right` selects pods with
-// 'foo', we should indicate the selectors overlap. It's a bit tricky to work
-// through all of the cases though, so we'll just punt for now.
-fn overlaps(left: &labels::Selector, right: &labels::Selector) -> bool {
-    if left.selects_all() || right.selects_all() {
-        return true;
-    }
-
-    left == right
-}
-
 fn json_response(rsp: AdmissionReview) -> Result<Response<Body>, Error> {
     let bytes = serde_json::to_vec(&rsp)?;
     Ok(Response::builder()
@@ -199,7 +186,7 @@ impl Validate<ServerSpec> for Admission {
         for server in servers.items.into_iter() {
             if server.name() != name
                 && server.spec.port == spec.port
-                && overlaps(&server.spec.pod_selector, &spec.pod_selector)
+                && Self::overlaps(&server.spec.pod_selector, &spec.pod_selector)
             {
                 bail!("identical server spec already exists");
             }
@@ -209,9 +196,42 @@ impl Validate<ServerSpec> for Admission {
     }
 }
 
+impl Admission {
+    /// Detects whether two pod selectors can select the same pod
+    //
+    // TODO(ver) We can probably detect overlapping selectors more effectively. For
+    // example, if `left` selects pods with 'foo=bar' and `right` selects pods with
+    // 'foo', we should indicate the selectors overlap. It's a bit tricky to work
+    // through all of the cases though, so we'll just punt for now.
+    fn overlaps(left: &labels::Selector, right: &labels::Selector) -> bool {
+        if left.selects_all() || right.selects_all() {
+            return true;
+        }
+
+        left == right
+    }
+}
+
 #[async_trait::async_trait]
 impl Validate<ServerAuthorizationSpec> for Admission {
     async fn validate(self, _ns: &str, _name: &str, spec: ServerAuthorizationSpec) -> Result<()> {
+        if let Some(mtls) = spec.client.mesh_tls.as_ref() {
+            if spec.client.unauthenticated {
+                bail!("`unauthenticated` must be false if `mesh_tls` is specified");
+            }
+            if mtls.unauthenticated_tls {
+                let ids = mtls.identities.as_ref().map(|ids| ids.len()).unwrap_or(0);
+                let sas = mtls
+                    .service_accounts
+                    .as_ref()
+                    .map(|sas| sas.len())
+                    .unwrap_or(0);
+                if ids + sas > 0 {
+                    bail!("`unauthenticatedTLS` be false if any `identities` or `service_accounts` is specified");
+                }
+            }
+        }
+
         for net in spec.client.networks.into_iter().flatten() {
             for except in net.except.into_iter().flatten() {
                 if except.contains(&net.cidr) {
diff --git a/viz/metrics-api/policy.go b/viz/metrics-api/policy.go
new file mode 100644
index 000000000..bf7546181
--- /dev/null
+++ b/viz/metrics-api/policy.go
@@ -0,0 +1,205 @@
+package api
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
+	"github.com/prometheus/common/model"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+const (
+	httpAuthzDenyQuery  = "sum(increase(inbound_http_authz_deny_total%s[%s])) by (%s)"
+	httpAuthzAllowQuery = "sum(increase(inbound_http_authz_allow_total%s[%s])) by (%s)"
+)
+
+func isPolicyResource(resource *pb.Resource) bool {
+	if resource != nil {
+		if resource.GetType() == k8s.Server || resource.GetType() == k8s.ServerAuthorization {
+			return true
+		}
+	}
+	return false
+}
+
+func (s *grpcServer) policyResourceQuery(ctx context.Context, req *pb.StatSummaryRequest) resourceResult {
+
+	policyResources, err := s.getPolicyResourceKeys(req)
+	if err != nil {
+		return resourceResult{res: nil, err: err}
+	}
+
+	var requestMetrics map[rKey]*pb.BasicStats
+	var tcpMetrics map[rKey]*pb.TcpStats
+	var authzMetrics map[rKey]*pb.ServerStats
+	if !req.SkipStats {
+		requestMetrics, tcpMetrics, authzMetrics, err = s.getPolicyMetrics(ctx, req, req.TimeWindow)
+		if err != nil {
+			return resourceResult{res: nil, err: err}
+		}
+	}
+
+	rows := make([]*pb.StatTable_PodGroup_Row, 0)
+	for _, key := range policyResources {
+		row := pb.StatTable_PodGroup_Row{
+			Resource: &pb.Resource{
+				Name:      key.Name,
+				Namespace: key.Namespace,
+				Type:      req.GetSelector().GetResource().GetType(),
+			},
+			TimeWindow: req.TimeWindow,
+			Stats:      requestMetrics[key],
+			TcpStats:   tcpMetrics[key],
+			SrvStats:   authzMetrics[key],
+		}
+
+		rows = append(rows, &row)
+	}
+
+	rsp := pb.StatTable{
+		Table: &pb.StatTable_PodGroup_{
+			PodGroup: &pb.StatTable_PodGroup{
+				Rows: rows,
+			},
+		},
+	}
+	return resourceResult{res: &rsp, err: nil}
+}
+
+func (s *grpcServer) getPolicyResourceKeys(req *pb.StatSummaryRequest) ([]rKey, error) {
+	var err error
+	var unstructuredResources *unstructured.UnstructuredList
+
+	// TODO(ver): We should use a typed client
+	var gvr schema.GroupVersionResource
+	if req.GetSelector().Resource.GetType() == k8s.Server {
+		gvr = k8s.ServerGVR
+	} else if req.GetSelector().Resource.GetType() == k8s.ServerAuthorization {
+		gvr = k8s.SazGVR
+	}
+
+	res := req.GetSelector().GetResource()
+	labelSelector, err := getLabelSelector(req)
+	if err != nil {
+		return nil, err
+	}
+
+	if res.GetNamespace() == "" {
+		unstructuredResources, err = s.k8sAPI.DynamicClient.Resource(gvr).Namespace("").
+			List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()})
+	} else if res.GetName() == "" {
+		unstructuredResources, err = s.k8sAPI.DynamicClient.Resource(gvr).Namespace(res.GetNamespace()).
+			List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()})
+	} else {
+		var ts *unstructured.Unstructured
+		ts, err = s.k8sAPI.DynamicClient.Resource(gvr).Namespace(res.GetNamespace()).
+			Get(context.TODO(), res.GetName(), metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+		unstructuredResources = &unstructured.UnstructuredList{Items: []unstructured.Unstructured{*ts}}
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	var resourceKeys []rKey
+	for _, resource := range unstructuredResources.Items {
+		// Resource Key's type should be singular and lowercased while the kind isn't
+		resourceKeys = append(resourceKeys, rKey{
+			Namespace: resource.GetNamespace(),
+			// TODO(ver) This isn't a reliable way to make a plural name singular.
+			Type: strings.ToLower(resource.GetKind()[0:len(resource.GetKind())]),
+			Name: resource.GetName(),
+		})
+	}
+	return resourceKeys, nil
+}
+
+func (s *grpcServer) getPolicyMetrics(
+	ctx context.Context,
+	req *pb.StatSummaryRequest,
+	timeWindow string,
+) (map[rKey]*pb.BasicStats, map[rKey]*pb.TcpStats, map[rKey]*pb.ServerStats, error) {
+	labels, groupBy := buildServerRequestLabels(req)
+	// These metrics are always inbound.
+	reqLabels := labels.Merge(model.LabelSet{
+		"direction": model.LabelValue("inbound"),
+	})
+
+	promQueries := make(map[promType]string)
+	if req.GetSelector().GetResource().GetType() == k8s.Server {
+		// TCP metrics are only supported with servers
+		if req.TcpStats {
+			// peer is always `src` as these are inbound metrics
+			tcpLabels := reqLabels.Merge(promPeerLabel("src"))
+			promQueries[promTCPConnections] = fmt.Sprintf(tcpConnectionsQuery, tcpLabels.String(), groupBy.String())
+			promQueries[promTCPReadBytes] = fmt.Sprintf(tcpReadBytesQuery, tcpLabels.String(), timeWindow, groupBy.String())
+			promQueries[promTCPWriteBytes] = fmt.Sprintf(tcpWriteBytesQuery, tcpLabels.String(), timeWindow, groupBy.String())
+		}
+	}
+
+	promQueries[promRequests] = fmt.Sprintf(reqQuery, reqLabels, timeWindow, groupBy.String())
+	// Use `labels` as direction isn't present with authorization metrics
+	promQueries[promAllowedRequests] = fmt.Sprintf(httpAuthzAllowQuery, labels, timeWindow, groupBy.String())
+	promQueries[promDeniedRequests] = fmt.Sprintf(httpAuthzDenyQuery, labels, timeWindow, groupBy.String())
+	quantileQueries := generateQuantileQueries(latencyQuantileQuery, reqLabels.String(), timeWindow, groupBy.String())
+	results, err := s.getPrometheusMetrics(ctx, promQueries, quantileQueries)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	basicStats, tcpStats, authzStats := processPrometheusMetrics(req, results, groupBy)
+	return basicStats, tcpStats, authzStats, nil
+}
+
+func buildServerRequestLabels(req *pb.StatSummaryRequest) (labels model.LabelSet, labelNames model.LabelNames) {
+	if req.GetSelector().GetResource().GetNamespace() != "" {
+		labels = labels.Merge(model.LabelSet{
+			namespaceLabel: model.LabelValue(req.GetSelector().GetResource().GetNamespace()),
+		})
+	}
+	var resourceGrouping model.LabelName
+	if req.GetSelector().GetResource().GetType() == k8s.Server {
+		resourceGrouping = serverNameLabel
+		labels = labels.Merge(model.LabelSet{
+			serverKindLabel: model.LabelValue("server"),
+		})
+		if req.GetSelector().GetResource().GetName() != "" {
+			labels = labels.Merge(model.LabelSet{
+				serverNameLabel: model.LabelValue(req.GetSelector().GetResource().GetName()),
+			})
+		}
+	} else if req.GetSelector().GetResource().GetType() == k8s.ServerAuthorization {
+		resourceGrouping = authorizationNameLabel
+		labels = labels.Merge(model.LabelSet{
+			authorizationKindLabel: model.LabelValue("serverauthorization"),
+		})
+		if req.GetSelector().GetResource().GetName() != "" {
+			labels = labels.Merge(model.LabelSet{
+				authorizationNameLabel: model.LabelValue(req.GetSelector().GetResource().GetName()),
+			})
+		}
+	}
+
+	switch out := req.Outbound.(type) {
+	case *pb.StatSummaryRequest_ToResource:
+		// if --to flag is passed, Calculate traffic sent to the policy resource
+		// with additional filtering narrowing down to the workload
+		// it is sent to.
+		labels = labels.Merge(promQueryLabels(out.ToResource))
+
+	// No FromResource case as policy metrics are all inbound
+	default:
+		// no extra labels needed
+	}
+
+	groupBy := model.LabelNames{namespaceLabel, resourceGrouping}
+
+	return labels, groupBy
+}
diff --git a/viz/metrics-api/prometheus.go b/viz/metrics-api/prometheus.go
index 2961caf65..120087ee8 100644
--- a/viz/metrics-api/prometheus.go
+++ b/viz/metrics-api/prometheus.go
@@ -36,14 +36,16 @@ const (
 	promLatencyP95      = promType("0.95")
 	promLatencyP99      = promType("0.99")
 
-	namespaceLabel           = model.LabelName("namespace")
-	dstNamespaceLabel        = model.LabelName("dst_namespace")
-	gatewayNameLabel         = model.LabelName("gateway_name")
-	gatewayNamespaceLabel    = model.LabelName("gateway_namespace")
-	remoteClusterNameLabel   = model.LabelName("target_cluster_name")
-	authorityLabel           = model.LabelName("authority")
-	serverLabel              = model.LabelName("srv_name")
-	serverAuthorizationLabel = model.LabelName("saz_name")
+	namespaceLabel         = model.LabelName("namespace")
+	dstNamespaceLabel      = model.LabelName("dst_namespace")
+	gatewayNameLabel       = model.LabelName("gateway_name")
+	gatewayNamespaceLabel  = model.LabelName("gateway_namespace")
+	remoteClusterNameLabel = model.LabelName("target_cluster_name")
+	authorityLabel         = model.LabelName("authority")
+	serverKindLabel        = model.LabelName("srv_kind")
+	serverNameLabel        = model.LabelName("srv_name")
+	authorizationKindLabel = model.LabelName("authz_kind")
+	authorizationNameLabel = model.LabelName("authz_name")
 )
 
 var (
@@ -117,9 +119,11 @@ func promQueryLabels(resource *pb.Resource) model.LabelSet {
 	if resource != nil {
 		if resource.Name != "" {
 			if resource.GetType() == k8s.Server {
-				set[serverLabel] = model.LabelValue(resource.GetName())
+				set[serverKindLabel] = model.LabelValue("server")
+				set[serverNameLabel] = model.LabelValue(resource.GetName())
 			} else if resource.GetType() == k8s.ServerAuthorization {
-				set[serverAuthorizationLabel] = model.LabelValue(resource.GetName())
+				set[authorizationKindLabel] = model.LabelValue("serverauthorization")
+				set[authorizationNameLabel] = model.LabelValue(resource.GetName())
 			} else if resource.GetType() != k8s.Service {
 				set[promResourceType(resource)] = model.LabelValue(resource.Name)
 			}
diff --git a/viz/metrics-api/stat_summary.go b/viz/metrics-api/stat_summary.go
index 604b31860..04af6c693 100644
--- a/viz/metrics-api/stat_summary.go
+++ b/viz/metrics-api/stat_summary.go
@@ -17,10 +17,8 @@ import (
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 )
 
 type resourceResult struct {
@@ -51,8 +49,6 @@ const (
 
 	reqQuery             = "sum(increase(response_total%s[%s])) by (%s, classification, tls)"
 	latencyQuantileQuery = "histogram_quantile(%s, sum(irate(response_latency_ms_bucket%s[%s])) by (le, %s))"
-	httpAuthzDenyQuery   = "sum(increase(inbound_http_authz_deny_total%s[%s])) by (%s)"
-	httpAuthzAllowQuery  = "sum(increase(inbound_http_authz_allow_total%s[%s])) by (%s)"
 	tcpConnectionsQuery  = "sum(tcp_open_connections%s) by (%s)"
 	tcpReadBytesQuery    = "sum(increase(tcp_read_bytes_total%s[%s])) by (%s)"
 	tcpWriteBytesQuery   = "sum(increase(tcp_write_bytes_total%s[%s])) by (%s)"
@@ -145,15 +141,6 @@ func (s *grpcServer) StatSummary(ctx context.Context, req *pb.StatSummaryRequest
 	return &rsp, nil
 }
 
-func isPolicyResource(resource *pb.Resource) bool {
-	if resource != nil {
-		if resource.GetType() == k8s.Server || resource.GetType() == k8s.ServerAuthorization {
-			return true
-		}
-	}
-	return false
-}
-
 func statSummaryError(req *pb.StatSummaryRequest, message string) *pb.StatSummaryResponse {
 	return &pb.StatSummaryResponse{
 		Response: &pb.StatSummaryResponse_Error{
@@ -273,91 +260,6 @@ func (s *grpcServer) k8sResourceQuery(ctx context.Context, req *pb.StatSummaryRe
 	return resourceResult{res: &rsp, err: nil}
 }
 
-func (s *grpcServer) getPolicyResourceKeys(req *pb.StatSummaryRequest) ([]rKey, error) {
-	var err error
-	var unstructuredResources *unstructured.UnstructuredList
-
-	var gvr schema.GroupVersionResource
-	if req.GetSelector().Resource.GetType() == k8s.Server {
-		gvr = k8s.ServerGVR
-	} else if req.GetSelector().Resource.GetType() == k8s.ServerAuthorization {
-		gvr = k8s.SazGVR
-	}
-
-	res := req.GetSelector().GetResource()
-	labelSelector, err := getLabelSelector(req)
-	if err != nil {
-		return nil, err
-	}
-
-	if res.GetNamespace() == "" {
-		unstructuredResources, err = s.k8sAPI.DynamicClient.Resource(gvr).Namespace("").List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()})
-	} else if res.GetName() == "" {
-		unstructuredResources, err = s.k8sAPI.DynamicClient.Resource(gvr).Namespace(res.GetNamespace()).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()})
-	} else {
-		var ts *unstructured.Unstructured
-		ts, err = s.k8sAPI.DynamicClient.Resource(gvr).Namespace(res.GetNamespace()).Get(context.TODO(), res.GetName(), metav1.GetOptions{})
-		if err != nil {
-			return nil, err
-		}
-		unstructuredResources = &unstructured.UnstructuredList{Items: []unstructured.Unstructured{*ts}}
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	var resourceKeys []rKey
-	for _, resource := range unstructuredResources.Items {
-		// Resource Key's type should be singular and lowercased while the kind isn't
-		resourceKeys = append(resourceKeys, rKey{Namespace: resource.GetNamespace(), Type: strings.ToLower(resource.GetKind()[0:len(resource.GetKind())]), Name: resource.GetName()})
-	}
-	return resourceKeys, nil
-}
-
-func (s *grpcServer) policyResourceQuery(ctx context.Context, req *pb.StatSummaryRequest) resourceResult {
-
-	policyResources, err := s.getPolicyResourceKeys(req)
-	if err != nil {
-		return resourceResult{res: nil, err: err}
-	}
-
-	var requestMetrics map[rKey]*pb.BasicStats
-	var tcpMetrics map[rKey]*pb.TcpStats
-	var authzMetrics map[rKey]*pb.ServerStats
-	if !req.SkipStats {
-		requestMetrics, tcpMetrics, authzMetrics, err = s.getPolicyMetrics(ctx, req, req.TimeWindow)
-		if err != nil {
-			return resourceResult{res: nil, err: err}
-		}
-	}
-
-	rows := make([]*pb.StatTable_PodGroup_Row, 0)
-	for _, key := range policyResources {
-		row := pb.StatTable_PodGroup_Row{
-			Resource: &pb.Resource{
-				Name:      key.Name,
-				Namespace: key.Namespace,
-				Type:      req.GetSelector().GetResource().GetType(),
-			},
-			TimeWindow: req.TimeWindow,
-			Stats:      requestMetrics[key],
-			TcpStats:   tcpMetrics[key],
-			SrvStats:   authzMetrics[key],
-		}
-
-		rows = append(rows, &row)
-	}
-
-	rsp := pb.StatTable{
-		Table: &pb.StatTable_PodGroup_{
-			PodGroup: &pb.StatTable_PodGroup{
-				Rows: rows,
-			},
-		},
-	}
-	return resourceResult{res: &rsp, err: nil}
-}
-
 func (s *grpcServer) serviceResourceQuery(ctx context.Context, req *pb.StatSummaryRequest) resourceResult {
 
 	rows := make([]*pb.StatTable_PodGroup_Row, 0)
@@ -564,42 +466,6 @@ func buildServiceRequestLabels(req *pb.StatSummaryRequest) (labels model.LabelSe
 	return labels, groupBy
 }
 
-func buildServerRequestLabels(req *pb.StatSummaryRequest) (labels model.LabelSet, labelNames model.LabelNames) {
-	if req.GetSelector().GetResource().GetNamespace() != "" {
-		labels = labels.Merge(model.LabelSet{
-			namespaceLabel: model.LabelValue(req.GetSelector().GetResource().GetNamespace()),
-		})
-	}
-	var resourceLabel model.LabelName
-	if req.GetSelector().GetResource().GetType() == k8s.Server {
-		resourceLabel = serverLabel
-	} else if req.GetSelector().GetResource().GetType() == k8s.ServerAuthorization {
-		resourceLabel = serverAuthorizationLabel
-	}
-
-	if req.GetSelector().GetResource().GetName() != "" {
-		labels = labels.Merge(model.LabelSet{
-			resourceLabel: model.LabelValue(req.GetSelector().GetResource().GetName()),
-		})
-	}
-
-	switch out := req.Outbound.(type) {
-	case *pb.StatSummaryRequest_ToResource:
-		// if --to flag is passed, Calculate traffic sent to the policy resource
-		// with additional filtering narrowing down to the workload
-		// it is sent to.
-		labels = labels.Merge(promQueryLabels(out.ToResource))
-
-	// No FromResource case as policy metrics are all inbound
-	default:
-		// no extra labels needed
-	}
-
-	groupBy := model.LabelNames{namespaceLabel, resourceLabel}
-
-	return labels, groupBy
-}
-
 func buildTCPStatsRequestLabels(req *pb.StatSummaryRequest, reqLabels model.LabelSet) string {
 	switch req.Outbound.(type) {
 	case *pb.StatSummaryRequest_ToResource, *pb.StatSummaryRequest_FromResource:
@@ -707,39 +573,6 @@ func (s *grpcServer) getServiceMetrics(ctx context.Context, req *pb.StatSummaryR
 	return dstBasicStats, dstTCPStats, nil
 }
 
-func (s *grpcServer) getPolicyMetrics(ctx context.Context, req *pb.StatSummaryRequest, timeWindow string) (map[rKey]*pb.BasicStats, map[rKey]*pb.TcpStats, map[rKey]*pb.ServerStats, error) {
-	labels, groupBy := buildServerRequestLabels(req)
-	// Server metrics are always inbound
-	reqLabels := labels.Merge(model.LabelSet{
-		"direction": model.LabelValue("inbound"),
-	})
-
-	promQueries := make(map[promType]string)
-	if req.GetSelector().GetResource().GetType() == k8s.Server {
-		// TCP metrics are only supported with servers
-		if req.TcpStats {
-			// peer is always `src` as these are inbound metrics
-			tcpLabels := reqLabels.Merge(promPeerLabel("src"))
-			promQueries[promTCPConnections] = fmt.Sprintf(tcpConnectionsQuery, tcpLabels.String(), groupBy.String())
-			promQueries[promTCPReadBytes] = fmt.Sprintf(tcpReadBytesQuery, tcpLabels.String(), timeWindow, groupBy.String())
-			promQueries[promTCPWriteBytes] = fmt.Sprintf(tcpWriteBytesQuery, tcpLabels.String(), timeWindow, groupBy.String())
-		}
-	}
-
-	promQueries[promRequests] = fmt.Sprintf(reqQuery, reqLabels, timeWindow, groupBy.String())
-	// Use `labels` as direction isn't present with authorization metrics
-	promQueries[promAllowedRequests] = fmt.Sprintf(httpAuthzAllowQuery, labels, timeWindow, groupBy.String())
-	promQueries[promDeniedRequests] = fmt.Sprintf(httpAuthzDenyQuery, labels, timeWindow, groupBy.String())
-	quantileQueries := generateQuantileQueries(latencyQuantileQuery, reqLabels.String(), timeWindow, groupBy.String())
-	results, err := s.getPrometheusMetrics(ctx, promQueries, quantileQueries)
-	if err != nil {
-		return nil, nil, nil, err
-	}
-
-	basicStats, tcpStats, authzStats := processPrometheusMetrics(req, results, groupBy)
-	return basicStats, tcpStats, authzStats, nil
-}
-
 func processPrometheusMetrics(req *pb.StatSummaryRequest, results []promResult, groupBy model.LabelNames) (map[rKey]*pb.BasicStats, map[rKey]*pb.TcpStats, map[rKey]*pb.ServerStats) {
 	basicStats := make(map[rKey]*pb.BasicStats)
 	tcpStats := make(map[rKey]*pb.TcpStats)
diff --git a/web/app/package.json b/web/app/package.json
index f885a8749..660216e9f 100644
--- a/web/app/package.json
+++ b/web/app/package.json
@@ -5,7 +5,7 @@
   "license": "MIT",
   "dependencies": {
     "@babel/eslint-plugin": "7.17.7",
-    "@fortawesome/fontawesome-svg-core": "6.1.0",
+    "@fortawesome/fontawesome-svg-core": "6.1.1",
     "@fortawesome/free-regular-svg-icons": "6.1.0",
     "@fortawesome/free-solid-svg-icons": "6.1.1",
     "@fortawesome/react-fontawesome": "0.1.18",
@@ -50,7 +50,7 @@
     "@lingui/cli": "3.13.2",
     "babel-core": "^7.0.0-bridge.0",
     "babel-jest": "^27.5.1",
-    "babel-loader": "^8.2.3",
+    "babel-loader": "^8.2.4",
     "babel-plugin-import": "^1.13.3",
     "babel-plugin-macros": "^3.1.0",
     "babel-plugin-transform-react-remove-prop-types": "0.4.24",
@@ -59,7 +59,7 @@
     "css-loader": "^6.7.1",
     "enzyme": "3.11.0",
     "enzyme-adapter-react-16": "^1.15.6",
-    "eslint": "^8.11.0",
+    "eslint": "^8.12.0",
     "eslint-config-airbnb": "^19.0.4",
     "eslint-plugin-import": "^2.25.4",
     "eslint-plugin-jsx-a11y": "^6.5.1",
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index d51060903..7d5c1c405 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -1088,12 +1088,12 @@
   resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105"
   integrity sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA==
 
-"@fortawesome/fontawesome-svg-core@6.1.0":
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.0.tgz#b78a43df315a6b1568d803ee48a9f1edea0714aa"
-  integrity sha512-racj+/EDnMZN0jcuHePOa+9kdHHOCpCAbBvVRnEi4G4DA5SWQiT/uXJ8WcfVEbLN51vPJjhukP4o+zH0cfYplg==
+"@fortawesome/fontawesome-svg-core@6.1.1":
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz#3424ec6182515951816be9b11665d67efdce5b5f"
+  integrity sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==
   dependencies:
-    "@fortawesome/fontawesome-common-types" "6.1.0"
+    "@fortawesome/fontawesome-common-types" "6.1.1"
 
 "@fortawesome/free-regular-svg-icons@6.1.0":
   version "6.1.0"
@@ -2333,13 +2333,13 @@ babel-jest@^27.5.1:
     graceful-fs "^4.2.9"
     slash "^3.0.0"
 
-babel-loader@^8.2.3:
-  version "8.2.3"
-  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.3.tgz#8986b40f1a64cacfcb4b8429320085ef68b1342d"
-  integrity sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==
+babel-loader@^8.2.4:
+  version "8.2.4"
+  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.4.tgz#95f5023c791b2e9e2ca6f67b0984f39c82ff384b"
+  integrity sha512-8dytA3gcvPPPv4Grjhnt8b5IIiTcq/zeXOPk4iTYI0SVXcsmuGg7JtBRDp8S9X+gJfhQ8ektjXZlDu1Bb33U8A==
   dependencies:
     find-cache-dir "^3.3.1"
-    loader-utils "^1.4.0"
+    loader-utils "^2.0.0"
     make-dir "^3.1.0"
     schema-utils "^2.6.5"
 
@@ -3792,10 +3792,10 @@ eslint-webpack-plugin@^3.1.1:
     normalize-path "^3.0.0"
     schema-utils "^3.1.1"
 
-eslint@^8.11.0:
-  version "8.11.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.11.0.tgz#88b91cfba1356fc10bb9eb592958457dfe09fb37"
-  integrity sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==
+eslint@^8.12.0:
+  version "8.12.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.12.0.tgz#c7a5bd1cfa09079aae64c9076c07eada66a46e8e"
+  integrity sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==
   dependencies:
     "@eslint/eslintrc" "^1.2.1"
     "@humanwhocodes/config-array" "^0.9.2"
@@ -5634,15 +5634,6 @@ loader-runner@^4.2.0:
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
   integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
 
-loader-utils@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
-  integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
-  dependencies:
-    big.js "^5.2.2"
-    emojis-list "^3.0.0"
-    json5 "^1.0.1"
-
 loader-utils@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"


Test patch:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index a9ea5eee4..ee1bcbe73 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index 1bf2c9a05..5bbf4ebda 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index a44d3abbb..e551fea63 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 1bf2c9a05..5bbf4ebda 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
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 72a2b5e23..bd5cd1c3f 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index 984a45f4d..df8ddbb73 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 72ca1cdb2..da2a91d05 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 9cb528517..ae8db9063 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 146678e3b..e80ff8e45 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_helm_crds_output.golden b/cli/cmd/testdata/install_helm_crds_output.golden
index 986154154..4e970a2bf 100644
--- a/cli/cmd/testdata/install_helm_crds_output.golden
+++ b/cli/cmd/testdata/install_helm_crds_output.golden
@@ -147,7 +147,7 @@ spec:
         description: The protocol of the server
         jsonPath: .spec.proxyProtocol
 ---
-# Source: linkerd-crds/templates/policy/serverauthorization.yaml
+# Source: linkerd-crds/templates/policy/server-authorization.yaml
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
@@ -202,9 +202,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -227,9 +224,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -254,10 +248,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -332,9 +322,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -357,9 +344,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -384,10 +368,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_helm_crds_output_ha.golden b/cli/cmd/testdata/install_helm_crds_output_ha.golden
index 986154154..4e970a2bf 100644
--- a/cli/cmd/testdata/install_helm_crds_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_crds_output_ha.golden
@@ -147,7 +147,7 @@ spec:
         description: The protocol of the server
         jsonPath: .spec.proxyProtocol
 ---
-# Source: linkerd-crds/templates/policy/serverauthorization.yaml
+# Source: linkerd-crds/templates/policy/server-authorization.yaml
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
@@ -202,9 +202,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -227,9 +224,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -254,10 +248,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -332,9 +322,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -357,9 +344,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -384,10 +368,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index c41c5140f..8089e2e85 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 72e19712e..730fd5e9f 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index ba1c3ff6a..15eaafab7 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index c15d45d75..68c6bb242 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -198,9 +198,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -223,9 +220,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -250,10 +244,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
@@ -328,9 +318,6 @@ spec:
                       description: >-
                         A label query over servers on which this authorization applies.
                       type: object
-                      oneOf:
-                        - required: [matchLabels]
-                        - required: [matchExpressions]
                       properties:
                         matchLabels:
                           type: object
@@ -353,9 +340,6 @@ spec:
                 client:
                   description:  Describes clients authorized to access a server.
                   type: object
-                  oneOf:
-                    - required: [meshTLS]
-                    - required: [unauthenticated]
                   properties:
                     networks:
                       description: >-
@@ -380,10 +364,6 @@ spec:
                       type: boolean
                     meshTLS:
                       type: object
-                      oneOf:
-                        - required: [unauthenticatedTLS]
-                        - required: [identities]
-                        - required: [serviceAccounts]
                       properties:
                         unauthenticatedTLS:
                           type: boolean
diff --git a/policy-controller/k8s/index/src/tests.rs b/policy-controller/k8s/index/src/tests.rs
index d5518a9c8..b4e3a27da 100644
--- a/policy-controller/k8s/index/src/tests.rs
+++ b/policy-controller/k8s/index/src/tests.rs
@@ -152,7 +152,7 @@ fn link_server_authz(selector: ServerSelector) {
         selector,
         k8s::policy::server_authorization::Client {
             networks: Some(vec![k8s::policy::server_authorization::Network {
-                cidr: "10.0.0.0/8".parse::<IpNet>().unwrap(),
+                cidr: "10.0.0.0/8".parse().unwrap(),
                 except: None,
             }]),
             unauthenticated: false,
diff --git a/policy-test/Cargo.toml b/policy-test/Cargo.toml
index a5da500c2..6ed695a1b 100644
--- a/policy-test/Cargo.toml
+++ b/policy-test/Cargo.toml
@@ -12,6 +12,7 @@ kube = { version = "0.70", default-features = false, features = ["client", "nati
 futures = { version = "0.3", default-features = false }
 ipnet = "2"
 k8s-openapi = { version = "0.14", features = ["v1_20"] }
+linkerd-policy-controller-core = { path = "../policy-controller/core" }
 linkerd-policy-controller-k8s-api = { path = "../policy-controller/k8s/api" }
 linkerd2-proxy-api = { version = "0.3", features = ["inbound", "client"] }
 maplit = "1"
diff --git a/policy-test/src/curl.rs b/policy-test/src/curl.rs
new file mode 100644
index 000000000..bf8a67238
--- /dev/null
+++ b/policy-test/src/curl.rs
@@ -0,0 +1,231 @@
+use super::{create, LinkerdInject};
+use kube::ResourceExt;
+use linkerd_policy_controller_k8s_api::{self as k8s};
+use maplit::{btreemap, convert_args};
+use tokio::time;
+
+#[derive(Clone)]
+#[must_use]
+pub struct Runner {
+    namespace: String,
+    client: kube::Client,
+}
+
+#[derive(Clone)]
+pub struct Running {
+    namespace: String,
+    name: String,
+    client: kube::Client,
+}
+
+impl Runner {
+    pub async fn init(client: &kube::Client, ns: &str) -> Runner {
+        let runner = Runner {
+            namespace: ns.to_string(),
+            client: client.clone(),
+        };
+        runner.create_rbac().await;
+        runner
+    }
+
+    /// Creates a configmap that prevents curl pods from executing.
+    pub async fn create_lock(&self) {
+        create(
+            &self.client,
+            k8s::api::core::v1::ConfigMap {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(self.namespace.clone()),
+                    name: Some("curl-lock".to_string()),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        )
+        .await;
+    }
+
+    /// Deletes the lock configmap, allowing curl pods to execute.
+    pub async fn delete_lock(&self) {
+        tracing::trace!(ns = %self.namespace, "Deleting curl-lock");
+        kube::Api::<k8s::api::core::v1::ConfigMap>::namespaced(
+            self.client.clone(),
+            &self.namespace,
+        )
+        .delete("curl-lock", &kube::api::DeleteParams::foreground())
+        .await
+        .expect("curl-lock must be deleted");
+        tracing::debug!(ns = %self.namespace, "Deleted curl-lock");
+    }
+
+    /// Runs a [`k8s::Pod`] that runs curl against the provided target URL.
+    ///
+    /// The pod:
+    /// - has the `linkerd.io/inject` annotation set, based on the
+    ///   `linkerd_inject` parameter;
+    /// - runs under the service account `curl`;
+    /// - does not actually execute curl until the `curl-lock` configmap is not
+    ///   present
+    pub async fn run(&self, name: &str, target_url: &str, inject: LinkerdInject) -> Running {
+        create(
+            &self.client,
+            Self::gen_pod(&self.namespace, name, target_url, inject),
+        )
+        .await;
+        Running {
+            client: self.client.clone(),
+            namespace: self.namespace.clone(),
+            name: name.to_string(),
+        }
+    }
+
+    /// Creates a service account and RBAC to allow curl pods to watch the
+    /// curl-lock configmap.
+    async fn create_rbac(&self) {
+        create(
+            &self.client,
+            k8s::api::core::v1::ServiceAccount {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(self.namespace.clone()),
+                    name: Some("curl".to_string()),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        )
+        .await;
+
+        create(
+            &self.client,
+            k8s::api::rbac::v1::Role {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(self.namespace.clone()),
+                    name: Some("curl-lock".to_string()),
+                    ..Default::default()
+                },
+                rules: Some(vec![k8s::api::rbac::v1::PolicyRule {
+                    api_groups: Some(vec!["".to_string()]),
+                    resources: Some(vec!["configmaps".to_string()]),
+                    verbs: vec!["get".to_string(), "list".to_string(), "watch".to_string()],
+                    ..Default::default()
+                }]),
+            },
+        )
+        .await;
+
+        create(
+            &self.client,
+            k8s::api::rbac::v1::RoleBinding {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(self.namespace.clone()),
+                    name: Some("curl-lock".to_string()),
+                    ..Default::default()
+                },
+                role_ref: k8s::api::rbac::v1::RoleRef {
+                    api_group: "rbac.authorization.k8s.io".to_string(),
+                    kind: "Role".to_string(),
+                    name: "curl-lock".to_string(),
+                },
+                subjects: Some(vec![k8s::api::rbac::v1::Subject {
+                    kind: "ServiceAccount".to_string(),
+                    name: "curl".to_string(),
+                    namespace: Some(self.namespace.clone()),
+                    ..Default::default()
+                }]),
+            },
+        )
+        .await;
+    }
+
+    fn gen_pod(ns: &str, name: &str, target_url: &str, inject: LinkerdInject) -> k8s::Pod {
+        k8s::Pod {
+            metadata: k8s::ObjectMeta {
+                namespace: Some(ns.to_string()),
+                name: Some(name.to_string()),
+                annotations: Some(convert_args!(btreemap!(
+                    "linkerd.io/inject" => inject.to_string(),
+                    "config.linkerd.io/proxy-log-level" => "linkerd=trace,info",
+                ))),
+                ..Default::default()
+            },
+            spec: Some(k8s::PodSpec {
+                service_account: Some("curl".to_string()),
+                init_containers: Some(vec![k8s::api::core::v1::Container {
+                    name: "wait-for-nginx".to_string(),
+                    image: Some("docker.io/bitnami/kubectl:latest".to_string()),
+                    args: Some(
+                        vec![
+                            "wait",
+                            "--timeout=60s",
+                            "--for=delete",
+                            "--namespace",
+                            ns,
+                            "cm",
+                            "curl-lock",
+                        ]
+                        .into_iter()
+                        .map(Into::into)
+                        .collect(),
+                    ),
+                    ..Default::default()
+                }]),
+                containers: vec![k8s::api::core::v1::Container {
+                    name: "curl".to_string(),
+                    image: Some("docker.io/curlimages/curl:latest".to_string()),
+                    args: Some(
+                        vec!["curl", "-sSfv", target_url]
+                            .into_iter()
+                            .map(Into::into)
+                            .collect(),
+                    ),
+                    ..Default::default()
+                }],
+                restart_policy: Some("Never".to_string()),
+                ..Default::default()
+            }),
+            ..k8s::Pod::default()
+        }
+    }
+}
+
+impl Running {
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    /// Waits for the pod to have an IP address and returns it.
+    pub async fn ip(&self) -> std::net::IpAddr {
+        super::await_pod_ip(&self.client, &self.namespace, &self.name).await
+    }
+
+    /// Waits for the curl container to complete and returns its exit code.
+    pub async fn exit_code(self) -> i32 {
+        fn get_exit_code(pod: &k8s::Pod) -> Option<i32> {
+            let c = pod
+                .status
+                .as_ref()?
+                .container_statuses
+                .as_ref()?
+                .iter()
+                .find(|c| c.name == "curl")?;
+            let code = c.state.as_ref()?.terminated.as_ref()?.exit_code;
+            tracing::debug!(ns = %pod.namespace().unwrap(), pod = %pod.name(), %code, "Curl exited");
+            Some(code)
+        }
+
+        tracing::debug!(ns = %self.namespace, pod = %self.name, "Waiting for exit code");
+        let api = kube::Api::namespaced(self.client.clone(), &self.namespace);
+        let finished = kube::runtime::wait::await_condition(
+            api.clone(),
+            &self.name,
+            |obj: Option<&k8s::Pod>| -> bool { obj.and_then(get_exit_code).is_some() },
+        );
+        match time::timeout(time::Duration::from_secs(60), finished).await {
+            Ok(Ok(())) => {}
+            Ok(Err(error)) => panic!("Failed to wait for exit code: {}: {}", self.name, error),
+            Err(_timeout) => panic!("Timeout waiting for exit code: {}", self.name),
+        };
+
+        let curl_pod = api.get(&self.name).await.expect("pod must exist");
+        get_exit_code(&curl_pod).expect("curl pod must have an exit code")
+    }
+}
diff --git a/policy-test/src/lib.rs b/policy-test/src/lib.rs
index 4da3d355e..8ee3e301e 100644
--- a/policy-test/src/lib.rs
+++ b/policy-test/src/lib.rs
@@ -2,12 +2,101 @@
 #![forbid(unsafe_code)]
 
 pub mod admission;
+pub mod curl;
 pub mod grpc;
+pub mod nginx;
 
 use linkerd_policy_controller_k8s_api::{self as k8s, ResourceExt};
 use maplit::{btreemap, convert_args};
+use tokio::time;
 use tracing::Instrument;
 
+#[derive(Copy, Clone, Debug)]
+pub enum LinkerdInject {
+    Enabled,
+    Disabled,
+}
+
+pub async fn create<T>(client: &kube::Client, obj: T) -> T
+where
+    T: kube::Resource + serde::Serialize + serde::de::DeserializeOwned + Clone + std::fmt::Debug,
+    T::DynamicType: Default,
+{
+    let params = kube::api::PostParams {
+        field_manager: Some("linkerd-policy-test".to_string()),
+        ..Default::default()
+    };
+    let api = match obj.namespace() {
+        Some(ns) => kube::Api::<T>::namespaced(client.clone(), &ns),
+        None => kube::Api::<T>::all(client.clone()),
+    };
+    tracing::trace!(?obj, "Creating");
+    api.create(&params, &obj)
+        .await
+        .expect("failed to create resource")
+}
+
+pub async fn await_condition<T>(
+    client: &kube::Client,
+    ns: &str,
+    name: &str,
+    cond: impl kube::runtime::wait::Condition<T>,
+) where
+    T: kube::Resource + serde::Serialize + serde::de::DeserializeOwned,
+    T: Clone + std::fmt::Debug + Send + 'static,
+    T::DynamicType: Default,
+{
+    let api = kube::Api::namespaced(client.clone(), ns);
+    time::timeout(
+        time::Duration::from_secs(60),
+        kube::runtime::wait::await_condition(api, name, cond),
+    )
+    .await
+    .expect("condition timed out")
+    .expect("API call failed")
+}
+
+/// Creates a pod and waits for all of its containers to be ready.
+pub async fn create_ready_pod(client: &kube::Client, pod: k8s::Pod) -> k8s::Pod {
+    let pod_ready = |obj: Option<&k8s::Pod>| -> bool {
+        if let Some(pod) = obj {
+            if let Some(status) = &pod.status {
+                if let Some(containers) = &status.container_statuses {
+                    return containers.iter().all(|c| c.ready);
+                }
+            }
+        }
+        false
+    };
+
+    let pod = create(client, pod).await;
+    await_condition(client, &pod.namespace().unwrap(), &pod.name(), pod_ready).await;
+    pod
+}
+
+pub async fn await_pod_ip(client: &kube::Client, ns: &str, name: &str) -> std::net::IpAddr {
+    fn get_ip(pod: &k8s::Pod) -> Option<String> {
+        pod.status.as_ref()?.pod_ip.clone()
+    }
+
+    await_condition(client, ns, name, |obj: Option<&k8s::Pod>| -> bool {
+        if let Some(pod) = obj {
+            return get_ip(pod).is_some();
+        }
+        false
+    })
+    .await;
+
+    let pod = kube::Api::namespaced(client.clone(), ns)
+        .get(name)
+        .await
+        .expect("must fetch pod");
+    get_ip(&pod)
+        .expect("pod must have an IP")
+        .parse()
+        .expect("pod IP must be valid")
+}
+
 /// Runs a test with a random namespace that is deleted on test completion
 pub async fn with_temp_ns<F, Fut>(test: F)
 where
@@ -22,26 +111,23 @@ where
         .expect("failed to initialize k8s client");
     let api = kube::Api::<k8s::Namespace>::all(client.clone());
 
-    let ns = k8s::Namespace {
-        metadata: k8s::ObjectMeta {
-            name: Some(format!("linkerd-policy-test-{}", random_suffix(6))),
-            labels: Some(convert_args!(btreemap!(
-                "linkerd-policy-test" => std::thread::current().name().unwrap_or(""),
-            ))),
+    let name = format!("linkerd-policy-test-{}", random_suffix(6));
+    tracing::debug!(namespace = %name, "Creating");
+    let ns = create(
+        &client,
+        k8s::Namespace {
+            metadata: k8s::ObjectMeta {
+                name: Some(name),
+                labels: Some(convert_args!(btreemap!(
+                    "linkerd-policy-test" => std::thread::current().name().unwrap_or(""),
+                ))),
+                ..Default::default()
+            },
             ..Default::default()
         },
-        ..Default::default()
-    };
-    tracing::debug!(namespace = %ns.name(), "Creating");
-    api.create(
-        &kube::api::PostParams {
-            dry_run: false,
-            field_manager: Some("linkerd-policy-test".to_string()),
-        },
-        &ns,
     )
-    .await
-    .expect("failed to create Namespace");
+    .await;
+    tracing::trace!(?ns);
 
     tracing::trace!("Spawning");
     let test = test(client.clone(), ns.name());
@@ -77,7 +163,7 @@ fn init_tracing() -> tracing::subscriber::DefaultGuard {
             .with_test_writer()
             .with_env_filter(
                 tracing_subscriber::EnvFilter::try_from_default_env()
-                    .unwrap_or_else(|_| "linkerd=trace,debug".parse().unwrap()),
+                    .unwrap_or_else(|_| "e2e=trace,api=trace,linkerd=trace,info".parse().unwrap()),
             )
             .finish(),
     )
@@ -113,3 +199,14 @@ impl rand::distributions::Distribution<u8> for LowercaseAlphanumeric {
         }
     }
 }
+
+// === imp LinkerdInject ===
+
+impl std::fmt::Display for LinkerdInject {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            LinkerdInject::Enabled => write!(f, "enabled"),
+            LinkerdInject::Disabled => write!(f, "disabled"),
+        }
+    }
+}
diff --git a/policy-test/src/nginx.rs b/policy-test/src/nginx.rs
new file mode 100644
index 000000000..f130a9cc7
--- /dev/null
+++ b/policy-test/src/nginx.rs
@@ -0,0 +1,69 @@
+use linkerd_policy_controller_k8s_api::{self as k8s};
+use maplit::{btreemap, convert_args};
+
+pub fn pod(ns: &str) -> k8s::Pod {
+    k8s::Pod {
+        metadata: k8s::ObjectMeta {
+            namespace: Some(ns.to_string()),
+            name: Some("nginx".to_string()),
+            annotations: Some(convert_args!(btreemap!(
+                "linkerd.io/inject" => "enabled",
+                "config.linkerd.io/proxy-log-level" => "linkerd=trace,info",
+            ))),
+            labels: Some(convert_args!(btreemap!(
+                "app" => "nginx",
+            ))),
+            ..Default::default()
+        },
+        spec: Some(k8s::PodSpec {
+            containers: vec![k8s::api::core::v1::Container {
+                name: "nginx".to_string(),
+                image: Some("docker.io/library/nginx:latest".to_string()),
+                ports: Some(vec![k8s::api::core::v1::ContainerPort {
+                    container_port: 80,
+                    ..Default::default()
+                }]),
+                ..Default::default()
+            }],
+            ..Default::default()
+        }),
+        ..k8s::Pod::default()
+    }
+}
+
+pub fn server(ns: &str) -> k8s::policy::Server {
+    k8s::policy::Server {
+        metadata: k8s::ObjectMeta {
+            namespace: Some(ns.to_string()),
+            name: Some("nginx".to_string()),
+            ..Default::default()
+        },
+        spec: k8s::policy::ServerSpec {
+            pod_selector: k8s::labels::Selector::from_iter(Some(("app", "nginx"))),
+            port: k8s::policy::server::Port::Number(80),
+            proxy_protocol: Some(k8s::policy::server::ProxyProtocol::Http1),
+        },
+    }
+}
+
+pub fn service(ns: &str) -> k8s::api::core::v1::Service {
+    k8s::api::core::v1::Service {
+        metadata: k8s::ObjectMeta {
+            namespace: Some(ns.to_string()),
+            name: Some("nginx".to_string()),
+            ..Default::default()
+        },
+        spec: Some(k8s::api::core::v1::ServiceSpec {
+            type_: Some("ClusterIP".to_string()),
+            selector: Some(convert_args!(btreemap!(
+                "app" => "nginx"
+            ))),
+            ports: Some(vec![k8s::api::core::v1::ServicePort {
+                port: 80,
+                ..Default::default()
+            }]),
+            ..Default::default()
+        }),
+        ..Default::default()
+    }
+}
diff --git a/policy-test/tests/admit_server_authorization.rs b/policy-test/tests/admit_server_authorization.rs
index fe5203c23..979dee397 100644
--- a/policy-test/tests/admit_server_authorization.rs
+++ b/policy-test/tests/admit_server_authorization.rs
@@ -64,6 +64,91 @@ async fn rejects_except_whole_cidr() {
     .await;
 }
 
+#[tokio::test(flavor = "current_thread")]
+async fn rejects_unauthenciated_and_mtls() {
+    admission::rejects(|ns| ServerAuthorization {
+        metadata: api::ObjectMeta {
+            namespace: Some(ns),
+            name: Some("test".to_string()),
+            ..Default::default()
+        },
+        spec: ServerAuthorizationSpec {
+            server: Server {
+                name: Some("test".to_string()),
+                selector: None,
+            },
+            client: Client {
+                unauthenticated: true,
+                mesh_tls: Some(MeshTls {
+                    identities: Some(vec!["*".to_string()]),
+                    ..Default::default()
+                }),
+                networks: None,
+            },
+        },
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn rejects_unauthenciated_tls_and_identities() {
+    admission::rejects(|ns| ServerAuthorization {
+        metadata: api::ObjectMeta {
+            namespace: Some(ns),
+            name: Some("test".to_string()),
+            ..Default::default()
+        },
+        spec: ServerAuthorizationSpec {
+            server: Server {
+                name: Some("test".to_string()),
+                selector: None,
+            },
+            client: Client {
+                mesh_tls: Some(MeshTls {
+                    unauthenticated_tls: true,
+                    identities: Some(vec!["*".to_string()]),
+                    ..Default::default()
+                }),
+                networks: None,
+                ..Default::default()
+            },
+        },
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn accepts_network_as_ip() {
+    admission::accepts(|ns| ServerAuthorization {
+        metadata: api::ObjectMeta {
+            namespace: Some(ns),
+            name: Some("test".to_string()),
+            ..Default::default()
+        },
+        spec: ServerAuthorizationSpec {
+            server: Server {
+                name: Some("test".to_string()),
+                selector: None,
+            },
+            client: Client {
+                networks: Some(vec![
+                    Network {
+                        cidr: "10.1.0.2".parse().unwrap(),
+                        except: None,
+                    },
+                    Network {
+                        cidr: "10.1.1.0/24".parse().unwrap(),
+                        except: Some(vec!["10.1.1.3".parse().unwrap()]),
+                    },
+                ]),
+                unauthenticated: true,
+                mesh_tls: None,
+            },
+        },
+    })
+    .await;
+}
+
 #[tokio::test(flavor = "current_thread")]
 async fn rejects_except_not_in_cidr() {
     admission::rejects(|ns| ServerAuthorization {
diff --git a/policy-test/tests/api.rs b/policy-test/tests/api.rs
index bd726fb0e..acc766a5b 100644
--- a/policy-test/tests/api.rs
+++ b/policy-test/tests/api.rs
@@ -1,8 +1,9 @@
 use futures::prelude::*;
-use kube::{runtime::wait::await_condition, ResourceExt};
+use kube::ResourceExt;
 use linkerd_policy_controller_k8s_api as k8s;
 use linkerd_policy_test::{
-    assert_is_default_all_unauthenticated, assert_protocol_detect, grpc, with_temp_ns,
+    assert_is_default_all_unauthenticated, assert_protocol_detect, create, create_ready_pod, grpc,
+    with_temp_ns,
 };
 use maplit::{btreemap, convert_args, hashmap};
 use tokio::time;
@@ -14,7 +15,7 @@ async fn server_with_server_authorization() {
     with_temp_ns(|client, ns| async move {
         // Create a pod that does nothing. It's injected with a proxy, so we can
         // attach policies to its admin server.
-        let pod = create_pod(&client, mk_pause(&ns, "pause")).await;
+        let pod = create_ready_pod(&client, mk_pause(&ns, "pause")).await;
         tracing::trace!(?pod);
 
         // Port-forward to the control plane and start watching the pod's admin
@@ -36,14 +37,7 @@ async fn server_with_server_authorization() {
 
         // Create a server that selects the pod's proxy admin server and ensure
         // that the update now uses this server, which has no authorizations
-        let server = kube::Api::<k8s::policy::Server>::namespaced(client.clone(), &ns)
-            .create(
-                &kube::api::PostParams::default(),
-                &mk_admin_server(&ns, "linkerd-admin"),
-            )
-            .await
-            .expect("server must apply");
-        tracing::trace!(?server);
+        let server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await;
         let config = rx
             .next()
             .await
@@ -67,31 +61,27 @@ async fn server_with_server_authorization() {
         // Create a server authorizaation that refers to the `linkerd-admin`
         // server (by name) and ensure that the update now reflects this
         // authorization.
-        let server_authz =
-            kube::Api::<k8s::policy::ServerAuthorization>::namespaced(client.clone(), &ns)
-                .create(
-                    &kube::api::PostParams::default(),
-                    &k8s::policy::ServerAuthorization {
-                        metadata: kube::api::ObjectMeta {
-                            namespace: Some(ns.clone()),
-                            name: Some("all-admin".to_string()),
-                            ..Default::default()
-                        },
-                        spec: k8s::policy::ServerAuthorizationSpec {
-                            server: k8s::policy::server_authorization::Server {
-                                name: Some("linkerd-admin".to_string()),
-                                selector: None,
-                            },
-                            client: k8s::policy::server_authorization::Client {
-                                unauthenticated: true,
-                                ..k8s::policy::server_authorization::Client::default()
-                            },
-                        },
+        create(
+            &client,
+            k8s::policy::ServerAuthorization {
+                metadata: kube::api::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("all-admin".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::ServerAuthorizationSpec {
+                    server: k8s::policy::server_authorization::Server {
+                        name: Some("linkerd-admin".to_string()),
+                        selector: None,
+                    },
+                    client: k8s::policy::server_authorization::Client {
+                        unauthenticated: true,
+                        ..k8s::policy::server_authorization::Client::default()
                     },
-                )
-                .await
-                .expect("serverauthorization must apply");
-        tracing::trace!(?server_authz);
+                },
+            },
+        )
+        .await;
         let config = rx
             .next()
             .await
@@ -108,7 +98,9 @@ async fn server_with_server_authorization() {
         );
         assert_eq!(
             config.authorizations.first().unwrap().labels,
-            convert_args!(hashmap!("name" => "all-admin")),
+            convert_args!(hashmap!(
+                "name" => "all-admin",
+            )),
         );
         assert_eq!(
             *config
@@ -147,49 +139,6 @@ async fn server_with_server_authorization() {
     .await;
 }
 
-async fn create<T>(client: &kube::Client, obj: T) -> T
-where
-    T: kube::Resource + serde::Serialize + serde::de::DeserializeOwned + Clone + std::fmt::Debug,
-    T::DynamicType: Default,
-{
-    kube::Api::<T>::namespaced(client.clone(), &obj.namespace().unwrap())
-        .create(
-            &kube::api::PostParams {
-                field_manager: Some("linkerd-policy-test".to_string()),
-                ..Default::default()
-            },
-            &obj,
-        )
-        .await
-        .expect("failed to create pod")
-}
-
-async fn create_pod(client: &kube::Client, pod: k8s::Pod) -> k8s::Pod {
-    let pod = create(client, pod).await;
-
-    let api = kube::Api::namespaced(client.clone(), &pod.namespace().unwrap());
-    time::timeout(
-        time::Duration::from_secs(60),
-        await_condition(api, &pod.name(), condition_pod_ready),
-    )
-    .await
-    .expect("pod failed to become ready")
-    .expect("pod failed to become ready");
-
-    pod
-}
-
-fn condition_pod_ready(obj: Option<&k8s::Pod>) -> bool {
-    if let Some(pod) = obj {
-        if let Some(status) = &pod.status {
-            if let Some(containers) = &status.container_statuses {
-                return containers.iter().all(|c| c.ready);
-            }
-        }
-    }
-    false
-}
-
 fn mk_pause(ns: &str, name: &str) -> k8s::Pod {
     k8s::Pod {
         metadata: k8s::ObjectMeta {
diff --git a/policy-test/tests/e2e_server_authorization.rs b/policy-test/tests/e2e_server_authorization.rs
new file mode 100644
index 000000000..39a517a48
--- /dev/null
+++ b/policy-test/tests/e2e_server_authorization.rs
@@ -0,0 +1,350 @@
+use linkerd_policy_controller_k8s_api::{
+    self as k8s, policy::server_authorization::Client as ClientAuthz, ResourceExt,
+};
+use linkerd_policy_test::{create, create_ready_pod, curl, nginx, with_temp_ns, LinkerdInject};
+
+#[tokio::test(flavor = "current_thread")]
+async fn meshtls() {
+    with_temp_ns(|client, ns| async move {
+        let srv = create(&client, nginx::server(&ns)).await;
+
+        create(
+            &client,
+            server_authz(
+                &ns,
+                "nginx",
+                &srv,
+                ClientAuthz {
+                    mesh_tls: Some(k8s::policy::server_authorization::MeshTls {
+                        identities: Some(vec!["*".to_string()]),
+                        ..Default::default()
+                    }),
+                    ..Default::default()
+                },
+            ),
+        )
+        .await;
+
+        // Create the nginx pod and wait for it to be ready.
+        tokio::join!(
+            create(&client, nginx::service(&ns)),
+            create_ready_pod(&client, nginx::pod(&ns))
+        );
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let (injected, uninjected) = tokio::join!(
+            curl.run("curl-injected", "http://nginx", LinkerdInject::Enabled),
+            curl.run("curl-uninjected", "http://nginx", LinkerdInject::Disabled),
+        );
+        let (injected_status, uninjected_status) =
+            tokio::join!(injected.exit_code(), uninjected.exit_code());
+        assert_eq!(injected_status, 0, "injected curl must succeed");
+        assert_eq!(uninjected_status, 22, "uninjected curl must fail");
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn network() {
+    with_temp_ns(|client, ns| async move {
+        let curl = curl::Runner::init(&client, &ns).await;
+
+        // Create a lock that prevents `curl` pods from running. This allows us
+        // to create a curl pod so that its IP can be obtained to be used in an
+        // authorization policy.
+        curl.create_lock().await;
+
+        // Create a curl pod and wait for it to get an IP.
+        let blessed = curl
+            .run("curl-blessed", "http://nginx", LinkerdInject::Disabled)
+            .await;
+        let blessed_ip = blessed.ip().await;
+        tracing::debug!(curl.blessed.ip = %blessed_ip);
+
+        // Once we know the IP of the (blocked) pod, create an nginx
+        // authorization policy that permits connections from this pod.
+        let srv = create(&client, nginx::server(&ns)).await;
+        create(
+            &client,
+            server_authz(
+                &ns,
+                "nginx",
+                &srv,
+                ClientAuthz {
+                    networks: Some(vec![k8s::policy::Network {
+                        cidr: blessed_ip.into(),
+                        except: None,
+                    }]),
+                    unauthenticated: true,
+                    ..Default::default()
+                },
+            ),
+        )
+        .await;
+
+        // Start nginx with the policy.
+        tokio::join!(
+            create(&client, nginx::service(&ns)),
+            create_ready_pod(&client, nginx::pod(&ns))
+        );
+
+        tracing::info!("Unblocking curl");
+        curl.delete_lock().await;
+
+        // The blessed pod should be able to connect to the nginx pod.
+        let status = blessed.exit_code().await;
+        assert_eq!(status, 0, "blessed curl must succeed");
+
+        // Create another curl pod that is not included in the authorization. It
+        // should fail to connect to the nginx pod.
+        let status = curl
+            .run("curl-cursed", "http://nginx", LinkerdInject::Disabled)
+            .await
+            .exit_code()
+            .await;
+        assert_eq!(status, 22, "cursed curl must fail");
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn both() {
+    with_temp_ns(|client, ns| async move {
+        let curl = curl::Runner::init(&client, &ns).await;
+
+        // Create a lock that prevents `curl` pods from running. This allows us
+        // to create a curl pod so that its IP can be obtained to be used in an
+        // authorization policy.
+        curl.create_lock().await;
+
+        let (blessed_injected, blessed_uninjected) = tokio::join!(
+            curl.run(
+                "curl-blessed-injected",
+                "http://nginx",
+                LinkerdInject::Enabled,
+            ),
+            curl.run(
+                "curl-blessed-uninjected",
+                "http://nginx",
+                LinkerdInject::Disabled,
+            )
+        );
+        let (blessed_injected_ip, blessed_uninjected_ip) =
+            tokio::join!(blessed_injected.ip(), blessed_uninjected.ip(),);
+        tracing::debug!(curl.blessed.injected.ip = ?blessed_injected_ip);
+        tracing::debug!(curl.blessed.uninjected.ip = ?blessed_uninjected_ip);
+
+        // Once we know the IP of the (blocked) pod, create an nginx
+        // authorization policy that permits connections from this pod.
+        let srv = create(&client, nginx::server(&ns)).await;
+        create(
+            &client,
+            server_authz(
+                &ns,
+                "nginx",
+                &srv,
+                ClientAuthz {
+                    networks: Some(vec![
+                        k8s::policy::Network {
+                            cidr: blessed_injected_ip.into(),
+                            except: None,
+                        },
+                        k8s::policy::Network {
+                            cidr: blessed_uninjected_ip.into(),
+                            except: None,
+                        },
+                    ]),
+                    mesh_tls: Some(k8s::policy::server_authorization::MeshTls {
+                        identities: Some(vec!["*".to_string()]),
+                        ..Default::default()
+                    }),
+                    ..Default::default()
+                },
+            ),
+        )
+        .await;
+
+        // Start nginx with the policy.
+        tokio::join!(
+            create(&client, nginx::service(&ns)),
+            create_ready_pod(&client, nginx::pod(&ns))
+        );
+
+        // Once the nginx pod is ready, delete the `curl-lock` configmap to
+        // unblock curl from running.
+        tracing::info!("Unblocking curl");
+        curl.delete_lock().await;
+
+        let (blessed_injected_status, blessed_uninjected_status) =
+            tokio::join!(blessed_injected.exit_code(), blessed_uninjected.exit_code());
+        // The blessed and injected pod should be able to connect to the nginx pod.
+        assert_eq!(
+            blessed_injected_status, 0,
+            "blessed injected curl must succeed"
+        );
+        // The blessed and uninjected pod should NOT be able to connect to the nginx pod.
+        assert_eq!(
+            blessed_uninjected_status, 22,
+            "blessed uninjected curl must fail"
+        );
+
+        let (cursed_injected, cursed_uninjected) = tokio::join!(
+            curl.run(
+                "curl-cursed-injected",
+                "http://nginx",
+                LinkerdInject::Enabled,
+            ),
+            curl.run(
+                "curl-cursed-uninjected",
+                "http://nginx",
+                LinkerdInject::Disabled,
+            )
+        );
+        let (cursed_injected_status, cursed_uninjected_status) =
+            tokio::join!(cursed_injected.exit_code(), cursed_uninjected.exit_code(),);
+        assert_eq!(cursed_injected_status, 22, "cursed injected curl must fail");
+        assert_eq!(
+            cursed_uninjected_status, 22,
+            "cursed uninjected curl must fail"
+        );
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn either() {
+    with_temp_ns(|client, ns| async move {
+        let curl = curl::Runner::init(&client, &ns).await;
+
+        tracing::info!("Blocking curl");
+        curl.create_lock().await;
+
+        let (blessed_injected, blessed_uninjected) = tokio::join!(
+            curl.run(
+                "curl-blessed-injected",
+                "http://nginx",
+                LinkerdInject::Enabled,
+            ),
+            curl.run(
+                "curl-blessed-uninjected",
+                "http://nginx",
+                LinkerdInject::Disabled,
+            )
+        );
+        let (blessed_injected_ip, blessed_uninjected_ip) =
+            tokio::join!(blessed_injected.ip(), blessed_uninjected.ip());
+        tracing::debug!(curl.blessed.injected.ip = ?blessed_injected_ip);
+        tracing::debug!(curl.blessed.uninjected.ip = ?blessed_uninjected_ip);
+
+        // Once we know the IP of the (blocked) pod, create an nginx
+        // authorization policy that permits connections from this pod.
+        let srv = create(&client, nginx::server(&ns)).await;
+        tokio::join!(
+            create(
+                &client,
+                server_authz(
+                    &ns,
+                    "nginx-from-ip",
+                    &srv,
+                    ClientAuthz {
+                        unauthenticated: true,
+                        networks: Some(vec![
+                            k8s::policy::Network {
+                                cidr: blessed_injected_ip.into(),
+                                except: None,
+                            },
+                            k8s::policy::Network {
+                                cidr: blessed_uninjected_ip.into(),
+                                except: None,
+                            },
+                        ]),
+                        ..Default::default()
+                    },
+                )
+            ),
+            create(
+                &client,
+                server_authz(
+                    &ns,
+                    "nginx-from-id",
+                    &srv,
+                    ClientAuthz {
+                        mesh_tls: Some(k8s::policy::server_authorization::MeshTls {
+                            identities: Some(vec!["*".to_string()]),
+                            ..Default::default()
+                        }),
+                        ..Default::default()
+                    },
+                )
+            ),
+        );
+
+        // Start nginx with the policy.
+        tokio::join!(
+            create(&client, nginx::service(&ns)),
+            create_ready_pod(&client, nginx::pod(&ns)),
+        );
+
+        tracing::info!("Unblocking curl");
+        curl.delete_lock().await;
+
+        let (blessed_injected_status, blessed_uninjected_status) =
+            tokio::join!(blessed_injected.exit_code(), blessed_uninjected.exit_code());
+        assert_eq!(
+            blessed_injected_status, 0,
+            "blessed injected curl must succeed"
+        );
+        assert_eq!(
+            blessed_uninjected_status, 0,
+            "blessed uninjected curl must succeed"
+        );
+
+        let (cursed_injected, cursed_uninjected) = tokio::join!(
+            curl.run(
+                "curl-cursed-injected",
+                "http://nginx",
+                LinkerdInject::Enabled,
+            ),
+            curl.run(
+                "curl-cursed-uninjected",
+                "http://nginx",
+                LinkerdInject::Disabled,
+            ),
+        );
+        let (cursed_injected_status, cursed_uninjected_status) =
+            tokio::join!(cursed_injected.exit_code(), cursed_uninjected.exit_code());
+        assert_eq!(
+            cursed_injected_status, 0,
+            "cursed injected curl must succeed"
+        );
+        assert_eq!(
+            cursed_uninjected_status, 22,
+            "cursed uninjected curl must fail"
+        );
+    })
+    .await;
+}
+
+// === helpers ===
+
+fn server_authz(
+    ns: &str,
+    name: &str,
+    target: &k8s::policy::Server,
+    client: ClientAuthz,
+) -> k8s::policy::ServerAuthorization {
+    k8s::policy::ServerAuthorization {
+        metadata: k8s::ObjectMeta {
+            namespace: Some(ns.to_string()),
+            name: Some(name.to_string()),
+            ..Default::default()
+        },
+        spec: k8s::policy::ServerAuthorizationSpec {
+            server: k8s::policy::server_authorization::Server {
+                name: Some(target.name()),
+                selector: None,
+            },
+            client,
+        },
+    }
+}
diff --git a/test/integration/deep/opaqueports/opaque_ports_test.go b/test/integration/deep/opaqueports/opaque_ports_test.go
index 883b9f93f..a519d8b2f 100644
--- a/test/integration/deep/opaqueports/opaque_ports_test.go
+++ b/test/integration/deep/opaqueports/opaque_ports_test.go
@@ -22,7 +22,7 @@ var (
 	opaqueUnmeshedSvcPod = "opaque-unmeshed-svc"
 	opaqueUnmeshedSvcSC  = "slow-cooker-opaque-unmeshed-svc"
 	tcpMetricRE          = regexp.MustCompile(
-		`tcp_open_total\{direction="inbound",peer="src",target_addr="[0-9\.]+:[0-9]+",target_ip="[0-9\.]+",target_port="[0-9]+",tls="true",client_id="default\.linkerd-opaque-ports-test\.serviceaccount\.identity\.linkerd\.cluster\.local",srv_name="default:all-unauthenticated".*} [0-9]+`,
+		`tcp_open_total\{direction="inbound",peer="src",target_addr="[0-9\.]+:[0-9]+",target_ip="[0-9\.]+",target_port="[0-9]+",tls="true",client_id="default\.linkerd-opaque-ports-test\.serviceaccount\.identity\.linkerd\.cluster\.local",srv_kind="default",srv_name="all-unauthenticated"} [0-9]+`,
 	)
 	tcpMetricOutUnmeshedRE = regexp.MustCompile(
 		`tcp_open_total\{direction="outbound",peer="dst",authority="[a-zA-Z\-]+\.[a-zA-Z\-]+\.svc\.cluster\.local:[0-9]+",target_addr="[0-9\.]+:[0-9]+",target_ip="[0-9\.]+",target_port="[0-9]+",tls="no_identity",no_tls_reason="not_provided_by_service_discovery",.*\} [0-9]+`,
diff --git a/test/integration/viz/policy/policy_test.go b/test/integration/viz/policy/policy_test.go
index 14a0d0795..933fc06b6 100644
--- a/test/integration/viz/policy/policy_test.go
+++ b/test/integration/viz/policy/policy_test.go
@@ -2,7 +2,6 @@ package policy
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"os"
 	"strconv"
@@ -147,13 +146,7 @@ func TestPolicy(t *testing.T) {
 					return nil
 				})
 				if err != nil {
-					// FIXME this test is flakey, and we may not get a success rate reported. If we
-					// hit that flakiness, just skip the test for now.
-					var ne noSuccess
-					if errors.As(err, &ne) {
-						t.Skipf("XXX Skipping flakey test: %s", err)
-					}
-					testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out checking stats (%s)", timeout), err)
+					testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out checking policy stats (%s)", timeout), err)
 				}
 			})
 		}