Repository: linkerd/linkerd2
Pull request #13390 (2024-11-25 10:14:16)

policy: Add e2e egress tests
This change adds e2e `EgressNetwork` tests that exercise:

- default policy
- explicit allows via route types
- routing of egress traffic to in-cluster services via backend refs

Signed-off-by: Zahari Dichev <zaharidichev@gmail.com>




Patch:
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 3e823cbb7..d3d7811c6 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -189,7 +189,7 @@ jobs:
           bin/scurl -v "https://raw.githubusercontent.com/k3d-io/k3d/${K3D_VERSION}/install.sh" | bash
           bin/scurl -vo /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" && chmod +x /usr/local/bin/yq
       - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab
-      - run: cargo install cargo-nextest
+      - run: cargo install cargo-nextest@0.9.67 --locked
       - run: just policy-test-build
       - run: just k3d-k8s='${{ matrix.k8s }}' k3d-create
       - run: docker load <image-archives/controller.tar
diff --git a/.proxy-version b/.proxy-version
index bc3745532..4364b4b36 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.268.0
+v2.269.0
diff --git a/Cargo.lock b/Cargo.lock
index a108503f5..b9efba1d7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -238,9 +238,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.8.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
+checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
 
 [[package]]
 name = "cc"
@@ -883,9 +883,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.13"
+version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
 
 [[package]]
 name = "jemalloc-sys"
@@ -1080,9 +1080,9 @@ dependencies = [
 
 [[package]]
 name = "kubert"
-version = "0.21.2"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "023388cc940f58816aa0d758a3fd4c4b837474ccd356803b7ac738b8a2f26a66"
+checksum = "3ff4135c85607c3733cd52e3098df704994980a91cb14afabd967a60cb167433"
 dependencies = [
  "ahash",
  "backoff",
@@ -1147,9 +1147,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "libc"
-version = "0.2.164"
+version = "0.2.166"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
+checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36"
 
 [[package]]
 name = "linked-hash-map"
@@ -2126,9 +2126,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
 
 [[package]]
 name = "socket2"
-version = "0.5.7"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
 dependencies = [
  "libc",
  "windows-sys",
@@ -2424,9 +2424,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
 
 [[package]]
 name = "tracing"
-version = "0.1.40"
+version = "0.1.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
 dependencies = [
  "log",
  "pin-project-lite",
@@ -2436,9 +2436,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-attributes"
-version = "0.1.27"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2447,9 +2447,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-core"
-version = "0.1.32"
+version = "0.1.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
 dependencies = [
  "once_cell",
  "valuable",
diff --git a/Cargo.toml b/Cargo.toml
index 2bd3689b0..4411f62aa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,3 +12,8 @@ members = [
 
 [profile.release]
 lto = "thin"
+
+[workspace.dependencies]
+k8s-openapi = { version = "0.20", features = ["v1_22"] }
+kube = { version = "0.87.1", default-features = false }
+kubert = { version = "0.22", default-features = false }
diff --git a/charts/linkerd-control-plane/templates/destination.yaml b/charts/linkerd-control-plane/templates/destination.yaml
index 41395c21a..21132ce9d 100644
--- a/charts/linkerd-control-plane/templates/destination.yaml
+++ b/charts/linkerd-control-plane/templates/destination.yaml
@@ -364,6 +364,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
           {{- with (.Values.policyController.livenessProbe).timeoutSeconds }}
           timeoutSeconds: {{ . }}
           {{- end }}
@@ -380,7 +381,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
           {{- with (.Values.policyController.readinessProbe).timeoutSeconds }}
           timeoutSeconds: {{ . }}
           {{- end }}
diff --git a/go.mod b/go.mod
index d019741ee..d4ff914a9 100644
--- a/go.mod
+++ b/go.mod
@@ -2,9 +2,6 @@ module github.com/linkerd/linkerd2
 
 go 1.22.0
 
-// Use the unreleased version of the proxy API to validate changes end-to-end.
-replace github.com/linkerd/linkerd2-proxy-api => github.com/linkerd/linkerd2-proxy-api v0.14.1-0.20241106205231-6c316cc41a3a
-
 require (
 	contrib.go.opencensus.io/exporter/ocagent v0.7.0
 	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
@@ -23,7 +20,7 @@ require (
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/imdario/mergo v0.3.16
 	github.com/julienschmidt/httprouter v1.3.0
-	github.com/linkerd/linkerd2-proxy-api v0.14.0
+	github.com/linkerd/linkerd2-proxy-api v0.15.0
 	github.com/mattn/go-isatty v0.0.20
 	github.com/mattn/go-runewidth v0.0.16
 	github.com/nsf/termbox-go v1.1.1
diff --git a/go.sum b/go.sum
index 2878bdeeb..4c4dc7b0a 100644
--- a/go.sum
+++ b/go.sum
@@ -295,8 +295,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
-github.com/linkerd/linkerd2-proxy-api v0.14.1-0.20241106205231-6c316cc41a3a h1:oVj8r0T7eUFLnwTvKmikeqedeJ8fnLE9keOdtpES8tE=
-github.com/linkerd/linkerd2-proxy-api v0.14.1-0.20241106205231-6c316cc41a3a/go.mod h1:gV0wEXbhnurC6HQPEI9Ea1f4gTfVXEJYC6MPVM21Xs8=
+github.com/linkerd/linkerd2-proxy-api v0.15.0 h1:PvFmstO+DQ00BSW86gqGMf8rLRsG+rnD4oBzDO4ISTc=
+github.com/linkerd/linkerd2-proxy-api v0.15.0/go.mod h1:6o9cU1AjX0JahLqROVyZ+ehy+1Vqj32xg4ZDBcntK1s=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
diff --git a/policy-controller/Cargo.toml b/policy-controller/Cargo.toml
index f00eeed06..daa80ec3b 100644
--- a/policy-controller/Cargo.toml
+++ b/policy-controller/Cargo.toml
@@ -17,7 +17,7 @@ anyhow = "1"
 async-trait = "0.1"
 drain = "0.1"
 futures = { version = "0.3", default-features = false }
-k8s-openapi = { version = "0.20", features = ["v1_22"] }
+k8s-openapi = { workspace = true }
 hyper = { version = "0.14", features = ["http1", "http2", "runtime", "server"] }
 ipnet = { version = "2", default-features = false }
 linkerd-policy-controller-core = { path = "./core" }
@@ -41,12 +41,12 @@ default-features = false
 features = ["derive", "env", "std"]
 
 [dependencies.kube]
-version = "0.87.1"
+workspace = true
 default-features = false
 features = ["admission", "derive"]
 
 [dependencies.kubert]
-version = "0.21.2"
+workspace = true
 default-features = false
 features = [
     "clap",
diff --git a/policy-controller/grpc/src/inbound.rs b/policy-controller/grpc/src/inbound.rs
index 946efe0e0..889418c74 100644
--- a/policy-controller/grpc/src/inbound.rs
+++ b/policy-controller/grpc/src/inbound.rs
@@ -145,7 +145,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server {
         kind: match srv.protocol {
             ProxyProtocol::Detect { timeout } => Some(proto::proxy_protocol::Kind::Detect(
                 proto::proxy_protocol::Detect {
-                    timeout: timeout.try_into().map_err(|error| tracing::warn!(%error, "failed to convert protocol detect timeout to protobuf")).ok(),
+                    timeout: timeout.try_into().map_err(|error| tracing::warn!(%error, "Failed to convert protocol detect timeout to protobuf")).ok(),
                     http_routes: http::to_route_list(&srv.http_routes, cluster_networks),
                     http_local_rate_limit: srv.ratelimit.as_ref().map(to_rate_limit),
                 },
diff --git a/policy-controller/grpc/src/outbound.rs b/policy-controller/grpc/src/outbound.rs
index 1c1c76001..404856e20 100644
--- a/policy-controller/grpc/src/outbound.rs
+++ b/policy-controller/grpc/src/outbound.rs
@@ -531,40 +531,49 @@ fn default_backend(policy: &OutboundPolicy, original_dst: Option<SocketAddr>) ->
                 },
             )),
         },
+
         ParentInfo::EgressNetwork {
             namespace, name, ..
-        } => match original_dst {
-            Some(original_dst) => outbound::Backend {
-                metadata: Some(Metadata {
-                    kind: Some(metadata::Kind::Resource(api::meta::Resource {
-                        group: "policy.linkerd.io".to_string(),
-                        kind: "EgressNetwork".to_string(),
-                        name,
-                        namespace,
-                        section: Default::default(),
-                        port: u16::from(policy.port).into(),
-                    })),
-                }),
+        } => {
+            debug_assert!(
+                original_dst.is_some(),
+                "Must not serve EgressNetwork for named lookups; IP:PORT required"
+            );
+            let metadata = Some(Metadata {
+                kind: Some(metadata::Kind::Resource(api::meta::Resource {
+                    group: "policy.linkerd.io".to_string(),
+                    kind: "EgressNetwork".to_string(),
+                    name,
+                    namespace,
+                    section: Default::default(),
+                    port: u16::from(policy.port).into(),
+                })),
+            });
+
+            let Some(addr) = original_dst else {
+                tracing::error!(
+                    ?metadata,
+                    "Unexpected state: EgressNetworks should only be returned when lookup is by IP:PORT; synthesizing invalid backend"
+                );
+                return outbound::Backend {
+                    metadata,
+                    queue: None,
+                    kind: None,
+                };
+            };
+
+            outbound::Backend {
+                metadata,
                 queue: Some(default_queue_config()),
                 kind: Some(outbound::backend::Kind::Forward(
                     destination::WeightedAddr {
-                        addr: Some(original_dst.into()),
+                        addr: Some(addr.into()),
                         weight: 1,
                         ..Default::default()
                     },
                 )),
-            },
-            None => {
-                tracing::error!("no original_dst for Egresspolicy");
-                outbound::Backend {
-                    metadata: Some(Metadata {
-                        kind: Some(metadata::Kind::Default("invalid".to_string())),
-                    }),
-                    queue: None,
-                    kind: None,
-                }
             }
-        },
+        }
     }
 }
 
@@ -632,7 +641,7 @@ pub(crate) fn convert_duration(
     duration
         .try_into()
         .map_err(|error| {
-            tracing::error!(%error, "Invalid {name} duration");
+            tracing::warn!(%error, "Invalid {name} duration");
         })
         .ok()
 }
diff --git a/policy-controller/grpc/src/outbound/grpc.rs b/policy-controller/grpc/src/outbound/grpc.rs
index 43159d5a7..3df4ef49f 100644
--- a/policy-controller/grpc/src/outbound/grpc.rs
+++ b/policy-controller/grpc/src/outbound/grpc.rs
@@ -454,11 +454,11 @@ fn convert_to_filter(filter: Filter) -> outbound::grpc_route::Filter {
                 convert_request_header_modifier_filter(filter),
             )),
             Filter::RequestRedirect(filter) => {
-                tracing::warn!(filter = ?filter, "declining to convert invalid filter type for GrpcRoute");
+                tracing::warn!(filter = ?filter, "Declining to convert invalid filter type for GrpcRoute");
                 None
             }
             Filter::ResponseHeaderModifier(filter) => {
-                tracing::warn!(filter = ?filter, "declining to convert invalid filter type for GrpcRoute");
+                tracing::warn!(filter = ?filter, "Declining to convert invalid filter type for GrpcRoute");
                 None
             }
         },
diff --git a/policy-controller/grpc/src/workload.rs b/policy-controller/grpc/src/workload.rs
index 813fccef9..b3ad096db 100644
--- a/policy-controller/grpc/src/workload.rs
+++ b/policy-controller/grpc/src/workload.rs
@@ -23,7 +23,7 @@ impl FromStr for Workload {
     fn from_str(s: &str) -> Result<Self, tonic::Status> {
         if s.starts_with('{') {
             return serde_json::from_str(s).map_err(|error| {
-                tracing::error!(%error, "Invalid {s} workload string");
+                tracing::warn!(%error, "Invalid {s} workload string");
                 tonic::Status::invalid_argument(format!("Invalid workload: {}", s))
             });
         }
diff --git a/policy-controller/k8s/api/Cargo.toml b/policy-controller/k8s/api/Cargo.toml
index ff3427b7f..507269ae0 100644
--- a/policy-controller/k8s/api/Cargo.toml
+++ b/policy-controller/k8s/api/Cargo.toml
@@ -6,11 +6,9 @@ license = "Apache-2.0"
 publish = false
 
 [dependencies]
-k8s-openapi = { version = "0.20", default-features = false, features = [
-    "v1_22",
-] }
+k8s-openapi = { workspace = true }
 k8s-gateway-api = { version = "0.16", features = ["experimental"] }
-kube = { version = "0.87.1", default-features = false, features = [
+kube = { workspace = true, default-features = false, features = [
     "client",
     "derive",
     "runtime",
diff --git a/policy-controller/k8s/api/src/labels.rs b/policy-controller/k8s/api/src/labels.rs
index e0e053fda..dc4e655b3 100644
--- a/policy-controller/k8s/api/src/labels.rs
+++ b/policy-controller/k8s/api/src/labels.rs
@@ -182,7 +182,7 @@ impl Expression {
             (Operator::Exists, key, None) => labels.contains_key(key),
             (Operator::DoesNotExist, key, None) => !labels.contains_key(key),
             (operator, key, values) => {
-                tracing::warn!(?operator, %key, ?values, "illegal match expression");
+                tracing::warn!(?operator, %key, ?values, "Illegal match expression");
                 false
             }
         }
diff --git a/policy-controller/k8s/index/Cargo.toml b/policy-controller/k8s/index/Cargo.toml
index b12340161..5194f5c02 100644
--- a/policy-controller/k8s/index/Cargo.toml
+++ b/policy-controller/k8s/index/Cargo.toml
@@ -11,12 +11,12 @@ anyhow = "1"
 chrono = { version = "0.4.38", default_features = false }
 futures = { version = "0.3", default-features = false }
 http = "0.2"
-kube = { version = "0.87.1", default-features = false, features = [
+kube = { workspace = true, default-features = false, features = [
     "client",
     "derive",
     "runtime",
 ] }
-kubert = { version = "0.21.2", default-features = false, features = ["index"] }
+kubert = { workspace = true, default-features = false, features = ["index"] }
 linkerd-policy-controller-core = { path = "../../core" }
 linkerd-policy-controller-k8s-api = { path = "../api" }
 parking_lot = "0.12"
diff --git a/policy-controller/k8s/index/src/inbound/index.rs b/policy-controller/k8s/index/src/inbound/index.rs
index 618d5de52..0aed1344e 100644
--- a/policy-controller/k8s/index/src/inbound/index.rs
+++ b/policy-controller/k8s/index/src/inbound/index.rs
@@ -478,7 +478,7 @@ impl kubert::index::IndexNamespacedResource<k8s::Pod> for Index {
             Ok(None) => {}
             Ok(Some(pod)) => pod.reindex_servers(&ns.policy, &self.authentications),
             Err(error) => {
-                tracing::error!(%error, "Illegal pod update");
+                tracing::warn!(%error, "Illegal pod update");
             }
         }
     }
@@ -523,7 +523,7 @@ impl kubert::index::IndexNamespacedResource<k8s::external_workload::ExternalWork
             // Update, so re-index
             Ok(Some(workload)) => workload.reindex_servers(&ns.policy, &self.authentications),
             Err(error) => {
-                tracing::error!(%error, "Illegal external workload update");
+                tracing::warn!(%error, "Illegal external workload update");
             }
         }
     }
@@ -626,7 +626,7 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::ServerAuthorization> fo
             Ok(meta) => self.ns_or_default_with_reindex(ns, move |ns| {
                 ns.policy.update_server_authz(name, meta)
             }),
-            Err(error) => tracing::error!(%error, "Illegal server authorization update"),
+            Err(error) => tracing::warn!(%error, "Illegal server authorization update"),
         }
     }
 
@@ -660,7 +660,7 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::ServerAuthorization> fo
                     .added
                     .push((name, saz)),
                 Err(error) => {
-                    tracing::error!(ns = %namespace, %name, %error, "Illegal server authorization update")
+                    tracing::warn!(ns = %namespace, %name, %error, "Illegal server authorization update")
                 }
             }
         }
@@ -746,7 +746,7 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::AuthorizationPolicy> fo
                     .added
                     .push((name, spec)),
                 Err(error) => {
-                    tracing::error!(ns = %namespace, %name, %error, "Illegal server authorization update")
+                    tracing::warn!(ns = %namespace, %name, %error, "Illegal server authorization update")
                 }
             }
         }
@@ -985,7 +985,7 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::HttpLocalRateLimitPolic
                     .added
                     .push((name, spec)),
                 Err(error) => {
-                    tracing::error!(ns = %namespace, %name, %error, "Illegal server ratelimit update")
+                    tracing::warn!(ns = %namespace, %name, %error, "Illegal server ratelimit update")
                 }
             }
         }
diff --git a/policy-controller/k8s/index/src/inbound/ratelimit_policy.rs b/policy-controller/k8s/index/src/inbound/ratelimit_policy.rs
index b3d77ce01..2d1672baa 100644
--- a/policy-controller/k8s/index/src/inbound/ratelimit_policy.rs
+++ b/policy-controller/k8s/index/src/inbound/ratelimit_policy.rs
@@ -82,7 +82,7 @@ impl TryFrom<k8s::policy::HttpLocalRateLimitPolicy> for Spec {
                 let type_ = match condition.type_.as_ref() {
                     "Accepted" => ConditionType::Accepted,
                     condition_type => {
-                        tracing::error!(%status.target_ref.name, %condition_type, "Unexpected condition type found in status");
+                        tracing::warn!(%status.target_ref.name, %condition_type, "Unexpected condition type found in status");
                         return None;
                     }
                 };
@@ -90,7 +90,7 @@ impl TryFrom<k8s::policy::HttpLocalRateLimitPolicy> for Spec {
                     "True" => true,
                     "False" => false,
                     condition_status => {
-                        tracing::error!(%status.target_ref.name, %type_, %condition_status, "Unexpected condition status found in status");
+                        tracing::warn!(%status.target_ref.name, %type_, %condition_status, "Unexpected condition status found in status");
                         return None
                     },
                 };
diff --git a/policy-controller/k8s/index/src/inbound/routes.rs b/policy-controller/k8s/index/src/inbound/routes.rs
index 5338b13ef..a96cc3bbb 100644
--- a/policy-controller/k8s/index/src/inbound/routes.rs
+++ b/policy-controller/k8s/index/src/inbound/routes.rs
@@ -137,7 +137,7 @@ impl Status {
                 let type_ = match condition.type_.as_ref() {
                     "Accepted" => ConditionType::Accepted,
                     condition_type => {
-                        tracing::error!(%status.parent_ref.name, %condition_type, "Unexpected condition type found in parent status");
+                        tracing::warn!(%status.parent_ref.name, %condition_type, "Unexpected condition type found in parent status");
                         return None;
                     }
                 };
@@ -145,7 +145,7 @@ impl Status {
                     "True" => true,
                     "False" => false,
                     condition_status => {
-                        tracing::error!(%status.parent_ref.name, %type_, %condition_status, "Unexpected condition status found in parent status");
+                        tracing::warn!(%status.parent_ref.name, %type_, %condition_status, "Unexpected condition status found in parent status");
                         return None
                     },
                 };
diff --git a/policy-controller/k8s/index/src/inbound/workload.rs b/policy-controller/k8s/index/src/inbound/workload.rs
index 233ec0a3f..8efb42e9f 100644
--- a/policy-controller/k8s/index/src/inbound/workload.rs
+++ b/policy-controller/k8s/index/src/inbound/workload.rs
@@ -103,7 +103,7 @@ fn container_http_probe_paths(
             match http::Uri::try_from(path) {
                 Ok(uri) => Some((port, uri.path().to_string())),
                 Err(error) => {
-                    tracing::warn!(%error, path, "invalid probe path");
+                    tracing::warn!(%error, path, "Invalid probe path");
                     None
                 }
             }
@@ -161,7 +161,7 @@ impl Settings {
         };
 
         let default_policy = default_policy(anns).unwrap_or_else(|error| {
-            tracing::warn!(%error, "invalid default policy annotation value");
+            tracing::warn!(%error, "Invalid default policy annotation value");
             None
         });
 
diff --git a/policy-controller/k8s/index/src/outbound/index.rs b/policy-controller/k8s/index/src/outbound/index.rs
index 4a8f53c17..910128e63 100644
--- a/policy-controller/k8s/index/src/outbound/index.rs
+++ b/policy-controller/k8s/index/src/outbound/index.rs
@@ -216,21 +216,21 @@ impl kubert::index::IndexNamespacedResource<Service> for Index {
         let ns = service.namespace().expect("Service must have a namespace");
         tracing::debug!(name, ns, "indexing service");
         let accrual = parse_accrual_config(service.annotations())
-            .map_err(|error| tracing::error!(%error, service=name, namespace=ns, "failed to parse accrual config"))
+            .map_err(|error| tracing::warn!(%error, service=name, namespace=ns, "Failed to parse accrual config"))
             .unwrap_or_default();
         let opaque_ports =
             ports_annotation(service.annotations(), "config.linkerd.io/opaque-ports")
                 .unwrap_or_else(|| self.namespaces.cluster_info.default_opaque_ports.clone());
 
         let timeouts = parse_timeouts(service.annotations())
-            .map_err(|error| tracing::error!(%error, service=name, namespace=ns, "failed to parse timeouts"))
+            .map_err(|error| tracing::warn!(%error, service=name, namespace=ns, "Failed to parse timeouts"))
             .unwrap_or_default();
 
         let http_retry = http::parse_http_retry(service.annotations()).map_err(|error| {
-            tracing::error!(%error, service=name, namespace=ns, "failed to parse http retry")
+            tracing::warn!(%error, service=name, namespace=ns, "Failed to parse http retry")
         }).unwrap_or_default();
         let grpc_retry = grpc::parse_grpc_retry(service.annotations()).map_err(|error| {
-            tracing::error!(%error, service=name, namespace=ns, "failed to parse grpc retry")
+            tracing::warn!(%error, service=name, namespace=ns, "Failed to parse grpc retry")
         }).unwrap_or_default();
 
         if let Some(cluster_ips) = service
@@ -252,7 +252,7 @@ impl kubert::index::IndexNamespacedResource<Service> for Index {
                         self.services_by_ip.insert(addr, service_ref);
                     }
                     Err(error) => {
-                        tracing::error!(%error, service=name, cluster_ip, "invalid cluster ip");
+                        tracing::warn!(%error, service=name, cluster_ip, "Invalid cluster ip");
                     }
                 }
             }
@@ -318,7 +318,7 @@ impl kubert::index::IndexNamespacedResource<linkerd_k8s_api::EgressNetwork> for
             .expect("EgressNetwork must have a namespace");
         tracing::debug!(name, ns, "indexing EgressNetwork");
         let accrual = parse_accrual_config(egress_network.annotations())
-            .map_err(|error| tracing::error!(%error, service=name, namespace=ns, "failed to parse accrual config"))
+            .map_err(|error| tracing::warn!(%error, service=name, namespace=ns, "Failed to parse accrual config"))
             .unwrap_or_default();
         let opaque_ports = ports_annotation(
             egress_network.annotations(),
@@ -327,14 +327,14 @@ impl kubert::index::IndexNamespacedResource<linkerd_k8s_api::EgressNetwork> for
         .unwrap_or_else(|| self.namespaces.cluster_info.default_opaque_ports.clone());
 
         let timeouts = parse_timeouts(egress_network.annotations())
-            .map_err(|error| tracing::error!(%error, service=name, namespace=ns, "failed to parse timeouts"))
+            .map_err(|error| tracing::warn!(%error, service=name, namespace=ns, "Failed to parse timeouts"))
             .unwrap_or_default();
 
         let http_retry = http::parse_http_retry(egress_network.annotations()).map_err(|error| {
-            tracing::error!(%error, service=name, namespace=ns, "failed to parse http retry")
+            tracing::warn!(%error, service=name, namespace=ns, "Failed to parse http retry")
         }).unwrap_or_default();
         let grpc_retry = grpc::parse_grpc_retry(egress_network.annotations()).map_err(|error| {
-            tracing::error!(%error, service=name, namespace=ns, "failed to parse grpc retry")
+            tracing::warn!(%error, service=name, namespace=ns, "Failed to parse grpc retry")
         }).unwrap_or_default();
 
         let egress_net_ref = ResourceRef {
@@ -678,7 +678,7 @@ impl Namespace {
         ) {
             Ok(route) => route,
             Err(error) => {
-                tracing::error!(%error, "failed to convert route");
+                tracing::warn!(%error, "Failed to convert route");
                 return;
             }
         };
@@ -773,7 +773,7 @@ impl Namespace {
         ) {
             Ok(route) => route,
             Err(error) => {
-                tracing::error!(%error, "failed to convert route");
+                tracing::warn!(%error, "Failed to convert route");
                 return;
             }
         };
@@ -868,7 +868,7 @@ impl Namespace {
             match tls::convert_route(&self.namespace, route.clone(), cluster_info, resource_info) {
                 Ok(route) => route,
                 Err(error) => {
-                    tracing::error!(%error, "failed to convert route");
+                    tracing::warn!(%error, "Failed to convert route");
                     return;
                 }
             };
@@ -964,7 +964,7 @@ impl Namespace {
             match tcp::convert_route(&self.namespace, route.clone(), cluster_info, resource_info) {
                 Ok(route) => route,
                 Err(error) => {
-                    tracing::error!(%error, "failed to convert route");
+                    tracing::warn!(%error, "Failed to convert route");
                     return;
                 }
             };
diff --git a/policy-controller/k8s/status/Cargo.toml b/policy-controller/k8s/status/Cargo.toml
index e8d6aaf6a..3b9ec9c51 100644
--- a/policy-controller/k8s/status/Cargo.toml
+++ b/policy-controller/k8s/status/Cargo.toml
@@ -10,7 +10,7 @@ ahash = "0.8"
 anyhow = "1"
 # Fix for https://github.com/chronotope/chrono/issues/602
 chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
-kubert = { version = "0.21.2", default-features = false, features = [
+kubert = { workspace = true, default-features = false, features = [
     "index",
     "lease",
 ] }
@@ -22,7 +22,7 @@ serde = "1"
 serde_json = "1.0.133"
 thiserror = "1"
 tokio = { version = "1", features = ["macros"] }
-tracing = "0.1.40"
+tracing = "0.1.41"
 
 [dev-dependencies.tokio]
 version = "1"
diff --git a/policy-controller/k8s/status/src/index.rs b/policy-controller/k8s/status/src/index.rs
index bea236672..a9c674816 100644
--- a/policy-controller/k8s/status/src/index.rs
+++ b/policy-controller/k8s/status/src/index.rs
@@ -60,9 +60,6 @@ pub struct Controller {
     updates: mpsc::Receiver<Update>,
     patch_timeout: Duration,
 
-    /// True if this policy controller is the leader — false otherwise.
-    leader: bool,
-
     metrics: ControllerMetrics,
 }
 
@@ -107,21 +104,21 @@ pub struct IndexMetrics {
     patch_channel_full: Counter,
 }
 
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Debug)]
 struct RouteRef {
     parents: Vec<routes::ParentReference>,
     backends: Vec<routes::BackendReference>,
     statuses: Vec<k8s_gateway_api::RouteParentStatus>,
 }
 
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Debug)]
 struct HttpLocalRateLimitPolicyRef {
     creation_timestamp: Option<DateTime<Utc>>,
     target_ref: ratelimit::TargetReference,
     status_conditions: Vec<k8s_core_api::Condition>,
 }
 
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Debug)]
 struct EgressNetworkRef {
     networks: Vec<Network>,
     status_conditions: Vec<k8s_core_api::Condition>,
@@ -236,7 +233,6 @@ impl Controller {
             name,
             updates,
             patch_timeout,
-            leader: false,
             metrics,
         }
     }
@@ -250,17 +246,20 @@ impl Controller {
         // now the leader. If so, we should apply the patches received;
         // otherwise, we should drain the updates queue but not apply any
         // patches since another policy controller is responsible for that.
+        let mut was_leader = false;
         loop {
+            // Refresh the state of the lease on each iteration to ensure we're
+            // checking expiration.
+            let is_leader = self.claims.borrow_and_update().is_current_for(&self.name);
+            if was_leader != is_leader {
+                tracing::info!(leader=%is_leader, "Status controller leadership change");
+            }
+            was_leader = is_leader;
+
             tokio::select! {
                 biased;
                 res = self.claims.changed() => {
                     res.expect("Claims watch must not be dropped");
-                    let claim = self.claims.borrow_and_update();
-                    let was_leader = self.leader;
-                    self.leader = claim.is_current_for(&self.name);
-                    if was_leader != self.leader {
-                        tracing::debug!(leader = %self.leader, "Leadership changed");
-                    }
                 }
 
                 Some(Update { id, patch}) = self.updates.recv() => {
@@ -268,23 +267,24 @@ impl Controller {
                     // If this policy controller is not the leader, it should
                     // process through the updates queue but not actually patch
                     // any resources.
-                    if self.leader {
-                        if id.gkn.group == linkerd_k8s_api::HttpRoute::group(&()) && id.gkn.kind == linkerd_k8s_api::HttpRoute::kind(&()){
-                            self.patch_status::<linkerd_k8s_api::HttpRoute>(&id.gkn.name, &id.namespace, patch).await;
-                        } else if id.gkn.group == k8s_gateway_api::HttpRoute::group(&()) && id.gkn.kind == k8s_gateway_api::HttpRoute::kind(&()) {
-                            self.patch_status::<k8s_gateway_api::HttpRoute>(&id.gkn.name, &id.namespace, patch).await;
-                        } else if id.gkn.group == k8s_gateway_api::GrpcRoute::group(&()) && id.gkn.kind == k8s_gateway_api::GrpcRoute::kind(&()) {
-                            self.patch_status::<k8s_gateway_api::GrpcRoute>(&id.gkn.name, &id.namespace, patch).await;
-                        } else if id.gkn.group == k8s_gateway_api::TcpRoute::group(&()) && id.gkn.kind == k8s_gateway_api::TcpRoute::kind(&()) {
-                            self.patch_status::<k8s_gateway_api::TcpRoute>(&id.gkn.name, &id.namespace, patch).await;
-                        } else if id.gkn.group == k8s_gateway_api::TlsRoute::group(&()) && id.gkn.kind == k8s_gateway_api::TlsRoute::kind(&()) {
-                            self.patch_status::<k8s_gateway_api::TlsRoute>(&id.gkn.name, &id.namespace, patch).await;
-                        } else if id.gkn.group == linkerd_k8s_api::HttpLocalRateLimitPolicy::group(&()) && id.gkn.kind == linkerd_k8s_api::HttpLocalRateLimitPolicy::kind(&()) {
-                            self.patch_status::<linkerd_k8s_api::HttpLocalRateLimitPolicy>(&id.gkn.name, &id.namespace, patch).await;
-                        } else if id.gkn.group == linkerd_k8s_api::EgressNetwork::group(&()) && id.gkn.kind == linkerd_k8s_api::EgressNetwork::kind(&()) {
-                            self.patch_status::<linkerd_k8s_api::EgressNetwork>(&id.gkn.name, &id.namespace, patch).await;
+                    if is_leader {
+                        if id.is_a::<linkerd_k8s_api::HttpRoute>() {
+                            self.patch::<linkerd_k8s_api::HttpRoute>(&id.gkn.name, &id.namespace, patch).await;
+                        } else if id.is_a::<k8s_gateway_api::HttpRoute>() {
+                            self.patch::<k8s_gateway_api::HttpRoute>(&id.gkn.name, &id.namespace, patch).await;
+                        } else if id.is_a::<k8s_gateway_api::GrpcRoute>() {
+                            self.patch::<k8s_gateway_api::GrpcRoute>(&id.gkn.name, &id.namespace, patch).await;
+                        } else if id.is_a::<k8s_gateway_api::TcpRoute>() {
+                            self.patch::<k8s_gateway_api::TcpRoute>(&id.gkn.name, &id.namespace, patch).await;
+                        } else if id.is_a::<k8s_gateway_api::TlsRoute>() {
+                            self.patch::<k8s_gateway_api::TlsRoute>(&id.gkn.name, &id.namespace, patch).await;
+                        } else if id.is_a::<linkerd_k8s_api::HttpLocalRateLimitPolicy>() {
+                            self.patch::<linkerd_k8s_api::HttpLocalRateLimitPolicy>(&id.gkn.name, &id.namespace, patch).await;
+                        } else if id.is_a::<linkerd_k8s_api::EgressNetwork>() {
+                            self.patch::<linkerd_k8s_api::EgressNetwork>(&id.gkn.name, &id.namespace, patch).await;
                         }
                     } else {
+                        tracing::debug!(?id, "Dropping patch because we are not the leader");
                         self.metrics.patch_drops.inc();
                     }
                 }
@@ -292,7 +292,15 @@ impl Controller {
         }
     }
 
-    async fn patch_status<K>(
+    #[tracing::instrument(
+        level = tracing::Level::ERROR,
+        skip(self, patch),
+        fields(
+            group=%K::group(&Default::default()),
+            kind=%K::kind(&Default::default()),
+        ),
+    )]
+    async fn patch<K>(
         &self,
         name: &str,
         namespace: &str,
@@ -302,32 +310,31 @@ impl Controller {
         <K as Resource>::DynamicType: Default,
         K: DeserializeOwned,
     {
-        let patch_params = k8s_core_api::PatchParams::apply(K::group(&Default::default()).as_ref());
+        tracing::trace!(?patch);
         let api = k8s_core_api::Api::<K>::namespaced(self.client.clone(), namespace);
+        let patch_params = k8s_core_api::PatchParams::apply(POLICY_CONTROLLER_NAME);
         let start = time::Instant::now();
-
-        match time::timeout(
+        let result = time::timeout(
             self.patch_timeout,
             api.patch_status(name, &patch_params, &patch),
         )
-        .await
-        {
+        .await;
+        let elapsed = start.elapsed();
+        tracing::trace!(?elapsed);
+        match result {
             Ok(Ok(_)) => {
                 self.metrics.patch_succeeded.inc();
-                self.metrics
-                    .patch_duration
-                    .observe(start.elapsed().as_secs_f64());
+                self.metrics.patch_duration.observe(elapsed.as_secs_f64());
+                tracing::info!("Patched status");
             }
             Ok(Err(error)) => {
                 self.metrics.patch_failed.inc();
-                self.metrics
-                    .patch_duration
-                    .observe(start.elapsed().as_secs_f64());
-                tracing::error!(%namespace, %name, kind = %K::kind(&Default::default()), %error, "Patch failed");
+                self.metrics.patch_duration.observe(elapsed.as_secs_f64());
+                tracing::error!(%error);
             }
             Err(_) => {
                 self.metrics.patch_timeout.inc();
-                tracing::error!(%namespace, %name, kind = %K::kind(&Default::default()), "Patch timed out");
+                tracing::error!("Timed out");
             }
         }
     }
@@ -363,31 +370,48 @@ impl Index {
     /// Controller applies patches or the write leaseholder changes, all
     /// routes have an up-to-date status.
     pub async fn run(index: Arc<RwLock<Self>>, reconciliation_period: Duration) {
-        // Clone the claims watch out of the index. This will immediately
-        // drop the read lock on the index so that it is not held for the
-        // lifetime of this function.
-        let mut claims = index.read().claims.clone();
+        // Extract what we need from the index so we don't need to lock it for
+        // housekeeping.
+        let (instance, mut claims) = {
+            let idx = index.read();
+            (idx.name.clone(), idx.claims.clone())
+        };
 
+        // The timer is reset when this instance becomes the leader and it is
+        // polled as long as it is the leader. The timer ensures that
+        // reconciliation happens at consistent intervals after leadership is
+        // acquired.
+        let mut timer = time::interval(reconciliation_period);
+        timer.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
+
+        let mut was_leader = false;
         loop {
+            // Refresh the state of the lease on each iteration to ensure we're
+            // checking expiration.
+            let is_leader = claims.borrow_and_update().is_current_for(&instance);
+            if is_leader && !was_leader {
+                tracing::debug!("Became leader; resetting timer");
+                timer.reset_immediately();
+            }
+            was_leader = is_leader;
+
             tokio::select! {
+                // Eagerly process claim updates to track leadership changes. If
+                // the claim changes, refesh the leadership status.
+                biased;
                 res = claims.changed() => {
                     res.expect("Claims watch must not be dropped");
-                    tracing::debug!("Lease holder has changed");
+                    if tracing::enabled!(tracing::Level::TRACE) {
+                        let c = claims.borrow();
+                        tracing::trace!(claim=?*c, "Changed");
+                    }
                 }
-                _ = time::sleep(reconciliation_period) => {}
-            }
 
-            // The claimant has changed, or we should attempt to reconcile all
-            //routes to account for any errors. In either case, we should
-            // only proceed if we are the current leader.
-            let claims = claims.borrow_and_update();
-            let index = index.read();
-
-            if !claims.is_current_for(&index.name) {
-                continue;
+                // Only wait for the timer if this instance is the leader.
+                _ = timer.tick(), if is_leader => {
+                    index.read().reconcile_if_leader();
+                }
             }
-            tracing::debug!(%index.name, "Lease holder reconciling cluster");
-            index.reconcile();
         }
     }
 
@@ -777,8 +801,33 @@ impl Index {
         make_patch(id, status)
     }
 
-    fn reconcile(&self) {
-        // first update all egress networks and their statuses
+    /// If this instance is the leader, reconcile the statuses for all resources
+    /// for which we control the status.
+    fn reconcile_if_leader(&self) {
+        let lease = self.claims.borrow();
+        if !lease.is_current_for(&self.name) {
+            tracing::trace!(%lease.holder, ?lease.expiry, "Reconcilation skipped");
+            return;
+        }
+        drop(lease);
+
+        tracing::trace!(
+            egressnetworks = self.egress_networks.len(),
+            routes = self.route_refs.len(),
+            httplocalratelimits = self.ratelimits.len(),
+            "Reconciling"
+        );
+        let egressnetworks = self.reconcile_egress_networks();
+        let routes = self.reconcile_routes();
+        let ratelimits = self.reconcile_ratelimits();
+
+        if egressnetworks + routes + ratelimits > 0 {
+            tracing::debug!(egressnetworks, routes, ratelimits, "Reconciled");
+        }
+    }
+
+    fn reconcile_egress_networks(&self) -> usize {
+        let mut patches = 0;
         for (id, net) in self.egress_networks.iter() {
             let id = NamespaceGroupKindName {
                 namespace: id.namespace.clone(),
@@ -795,6 +844,7 @@ impl Index {
                     patch,
                 }) {
                     Ok(()) => {
+                        patches += 1;
                         self.metrics.patch_enqueues.inc();
                     }
                     Err(error) => {
@@ -804,8 +854,11 @@ impl Index {
                 }
             }
         }
+        patches
+    }
 
-        // then update all route refs
+    fn reconcile_routes(&self) -> usize {
+        let mut patches = 0;
         for (id, route) in self.route_refs.iter() {
             if let Some(patch) = self.make_route_patch(id, route) {
                 match self.updates.try_send(Update {
@@ -813,6 +866,7 @@ impl Index {
                     patch,
                 }) {
                     Ok(()) => {
+                        patches += 1;
                         self.metrics.patch_enqueues.inc();
                     }
                     Err(error) => {
@@ -822,8 +876,11 @@ impl Index {
                 }
             }
         }
+        patches
+    }
 
-        // then update all ratelimit policies and their statuses
+    fn reconcile_ratelimits(&self) -> usize {
+        let mut patches = 0;
         for (id, rl) in self.ratelimits.iter() {
             let id = NamespaceGroupKindName {
                 namespace: id.namespace.clone(),
@@ -840,6 +897,7 @@ impl Index {
                     patch,
                 }) {
                     Ok(()) => {
+                        patches += 1;
                         self.metrics.patch_enqueues.inc();
                     }
                     Err(error) => {
@@ -849,6 +907,43 @@ impl Index {
                 }
             }
         }
+        patches
+    }
+
+    #[tracing::instrument(level = "debug", skip(self, net))]
+    fn index_egress_network(&mut self, id: ResourceId, net: EgressNetworkRef) {
+        tracing::trace!(?net);
+        // Insert into the index; if the network is already in the index, and it hasn't
+        // changed, skip creating a patch.
+        if !self.update_egress_net(id, &net) {
+            return;
+        }
+
+        self.reconcile_if_leader();
+    }
+
+    #[tracing::instrument(level = "debug", skip(self, route))]
+    fn index_route(&mut self, id: NamespaceGroupKindName, route: RouteRef) {
+        tracing::trace!(?route);
+        // Insert into the index; if the route is already in the index, and it hasn't
+        // changed, skip creating a patch.
+        if !self.update_route(id.clone(), &route) {
+            return;
+        }
+
+        self.reconcile_if_leader();
+    }
+
+    #[tracing::instrument(level = "debug", skip(self, ratelimit))]
+    fn index_ratelimit(&mut self, id: ResourceId, ratelimit: HttpLocalRateLimitPolicyRef) {
+        tracing::trace!(?ratelimit);
+        // Insert into the index; if the route is already in the index, and it hasn't
+        // changed, skip creating a patch.
+        if !self.update_ratelimit(id.clone(), &ratelimit) {
+            return;
+        }
+
+        self.reconcile_if_leader();
     }
 }
 
@@ -1158,29 +1253,13 @@ impl kubert::index::IndexNamespacedResource<linkerd_k8s_api::Server> for Index {
     fn apply(&mut self, resource: linkerd_k8s_api::Server) {
         let namespace = resource.namespace().expect("Server must have a namespace");
         let name = resource.name_unchecked();
-        let id = ResourceId::new(namespace, name);
-
-        self.servers.insert(id);
-
-        // If we're not the leader, skip reconciling the cluster.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-        self.reconcile();
+        self.servers.insert(ResourceId::new(namespace, name));
+        self.reconcile_if_leader();
     }
 
     fn delete(&mut self, namespace: String, name: String) {
-        let id = ResourceId::new(namespace, name);
-
-        self.servers.remove(&id);
-
-        // If we're not the leader, skip reconciling the cluster.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-        self.reconcile();
+        self.servers.remove(&ResourceId::new(namespace, name));
+        self.reconcile_if_leader();
     }
 
     // Since apply only reindexes a single Server at a time, there's no need
@@ -1191,29 +1270,14 @@ impl kubert::index::IndexNamespacedResource<k8s_core_api::Service> for Index {
     fn apply(&mut self, resource: k8s_core_api::Service) {
         let namespace = resource.namespace().expect("Service must have a namespace");
         let name = resource.name_unchecked();
-        let id = ResourceId::new(namespace, name);
-
-        self.services.insert(id, resource.into());
-
-        // If we're not the leader, skip reconciling the cluster.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-        self.reconcile();
+        self.services
+            .insert(ResourceId::new(namespace, name), resource.into());
+        self.reconcile_if_leader();
     }
 
     fn delete(&mut self, namespace: String, name: String) {
-        let id = ResourceId::new(namespace, name);
-
-        self.services.remove(&id);
-
-        // If we're not the leader, skip reconciling the cluster.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-        self.reconcile();
+        self.services.remove(&ResourceId::new(namespace, name));
+        self.reconcile_if_leader();
     }
 
     // Since apply only reindexes a single Service at a time, there's no need
@@ -1249,13 +1313,7 @@ impl kubert::index::IndexNamespacedResource<linkerd_k8s_api::HttpLocalRateLimitP
     fn delete(&mut self, namespace: String, name: String) {
         let id = ResourceId::new(namespace, name);
         self.ratelimits.remove(&id);
-
-        // If we're not the leader, skip reconciling the cluster.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-        self.reconcile();
+        self.reconcile_if_leader();
     }
 }
 
@@ -1304,66 +1362,7 @@ impl kubert::index::IndexNamespacedResource<linkerd_k8s_api::EgressNetwork> for
     fn delete(&mut self, namespace: String, name: String) {
         let id = ResourceId::new(namespace, name);
         self.egress_networks.remove(&id);
-
-        // If we're not the leader, skip reconciling the cluster.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-        self.reconcile();
-    }
-}
-
-impl Index {
-    fn index_egress_network(&mut self, id: ResourceId, net: EgressNetworkRef) {
-        // Insert into the index; if the network is already in the index, and it hasn't
-        // changed, skip creating a patch.
-        if !self.update_egress_net(id, &net) {
-            return;
-        }
-
-        // If we're not the leader, skip creating a patch and sending an
-        // update to the Controller.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-
-        self.reconcile()
-    }
-
-    fn index_route(&mut self, id: NamespaceGroupKindName, route: RouteRef) {
-        // Insert into the index; if the route is already in the index, and it hasn't
-        // changed, skip creating a patch.
-        if !self.update_route(id.clone(), &route) {
-            return;
-        }
-
-        // If we're not the leader, skip creating a patch and sending an
-        // update to the Controller.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-
-        self.reconcile()
-    }
-
-    fn index_ratelimit(&mut self, id: ResourceId, ratelimit: HttpLocalRateLimitPolicyRef) {
-        // Insert into the index; if the route is already in the index, and it hasn't
-        // changed, skip creating a patch.
-        if !self.update_ratelimit(id.clone(), &ratelimit) {
-            return;
-        }
-
-        // If we're not the leader, skip creating a patch and sending an
-        // update to the Controller.
-        if !self.claims.borrow().is_current_for(&self.name) {
-            tracing::debug!(%self.name, "Lease non-holder skipping controller update");
-            return;
-        }
-
-        self.reconcile()
+        self.reconcile_if_leader();
     }
 }
 
@@ -1376,7 +1375,7 @@ where
 {
     match resource_id.api_version() {
         Err(error) => {
-            tracing::error!(error = %error, "failed to create patch for resource");
+            tracing::error!(error = %error, "Failed to create patch for resource");
             None
         }
         Ok(api_version) => {
diff --git a/policy-controller/k8s/status/src/ratelimit.rs b/policy-controller/k8s/status/src/ratelimit.rs
index f8e8bce83..26258dc51 100644
--- a/policy-controller/k8s/status/src/ratelimit.rs
+++ b/policy-controller/k8s/status/src/ratelimit.rs
@@ -1,7 +1,7 @@
 use crate::resource_id::ResourceId;
 use linkerd_policy_controller_k8s_api::policy as linkerd_k8s_api;
 
-#[derive(Clone, Eq, PartialEq)]
+#[derive(Clone, Eq, PartialEq, Debug)]
 pub enum TargetReference {
     Server(ResourceId),
     UnknownKind,
diff --git a/policy-controller/k8s/status/src/resource_id.rs b/policy-controller/k8s/status/src/resource_id.rs
index f930fe02b..e5c515a14 100644
--- a/policy-controller/k8s/status/src/resource_id.rs
+++ b/policy-controller/k8s/status/src/resource_id.rs
@@ -42,4 +42,11 @@ impl NamespaceGroupKindName {
             }
         }
     }
+
+    pub fn is_a<K>(&self) -> bool
+    where
+        K: Resource<DynamicType = ()>,
+    {
+        self.gkn.group == K::group(&()) && self.gkn.kind == K::kind(&())
+    }
 }
diff --git a/policy-controller/k8s/status/src/routes.rs b/policy-controller/k8s/status/src/routes.rs
index c2076d9aa..baca6b872 100644
--- a/policy-controller/k8s/status/src/routes.rs
+++ b/policy-controller/k8s/status/src/routes.rs
@@ -18,7 +18,7 @@ pub(crate) mod tls;
 /// namespace. This is something that should be relaxed in the future in the
 /// policy controller's index, and we could then consider consolidating these
 /// types into a single shared lib.
-#[derive(Clone, Eq, PartialEq)]
+#[derive(Clone, Eq, PartialEq, Debug)]
 pub enum ParentReference {
     Server(ResourceId),
     Service(ResourceId, Option<u16>),
@@ -26,7 +26,7 @@ pub enum ParentReference {
     UnknownKind,
 }
 
-#[derive(Clone, Eq, PartialEq)]
+#[derive(Clone, Eq, PartialEq, Debug)]
 pub enum BackendReference {
     Service(ResourceId),
     EgressNetwork(ResourceId),
diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs
index efd38f1f1..71cd8df79 100644
--- a/policy-controller/src/admission.rs
+++ b/policy-controller/src/admission.rs
@@ -77,7 +77,7 @@ impl hyper::service::Service<Request<Body>> for Admission {
             let review: Review = match serde_json::from_reader(bytes.reader()) {
                 Ok(review) => review,
                 Err(error) => {
-                    warn!(%error, "failed to parse request body");
+                    warn!(%error, "Failed to parse request body");
                     return json_response(AdmissionResponse::invalid(error).into_review());
                 }
             };
@@ -89,7 +89,7 @@ impl hyper::service::Service<Request<Body>> for Admission {
                     admission.admit(req).await
                 }
                 Err(error) => {
-                    warn!(%error, "invalid admission request");
+                    warn!(%error, "Invalid admission request");
                     AdmissionResponse::invalid(error)
                 }
             };
diff --git a/policy-controller/src/lib.rs b/policy-controller/src/lib.rs
index 4a0fc2408..4ad18eca8 100644
--- a/policy-controller/src/lib.rs
+++ b/policy-controller/src/lib.rs
@@ -94,7 +94,7 @@ impl DiscoverOutboundPolicy<ResourceTarget, OutboundDiscoverTarget> for Outbound
         let rx = match self.0.write().outbound_policy_rx(resource.clone()) {
             Ok(rx) => rx,
             Err(error) => {
-                tracing::error!(%error, "failed to get outbound policy rx");
+                tracing::error!(%error, "Failed to get outbound policy rx");
                 return Ok(None);
             }
         };
diff --git a/policy-controller/src/main.rs b/policy-controller/src/main.rs
index 23c2b5e69..1bc747bed 100644
--- a/policy-controller/src/main.rs
+++ b/policy-controller/src/main.rs
@@ -360,7 +360,7 @@ async fn main() -> Result<()> {
     // Spawn the status Controller reconciliation.
     tokio::spawn(
         status::Index::run(status_index.clone(), RECONCILIATION_PERIOD)
-            .instrument(info_span!("status::Index")),
+            .instrument(info_span!("status_index")),
     );
 
     // Run the gRPC server, serving results by looking up against the index handle.
@@ -386,7 +386,7 @@ async fn main() -> Result<()> {
     tokio::spawn(
         status_controller
             .run()
-            .instrument(info_span!("status::Controller")),
+            .instrument(info_span!("status_controller")),
     );
 
     let client = runtime.client();
@@ -495,10 +495,10 @@ async fn init_lease(client: Client, ns: &str, deployment_name: &str) -> Result<L
         )
         .await
     {
-        Ok(lease) => tracing::info!(?lease, "created Lease resource"),
-        Err(k8s::Error::Api(_)) => tracing::info!("Lease already exists, no need to create it"),
+        Ok(lease) => tracing::info!(?lease, "Created Lease resource"),
+        Err(k8s::Error::Api(_)) => tracing::debug!("Lease already exists, no need to create it"),
         Err(error) => {
-            tracing::error!(%error, "error creating Lease resource");
+            tracing::error!(%error, "Failed to create Lease resource");
             return Err(error.into());
         }
     };


Test patch:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index e015bbd81..979a25c05 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -1633,6 +1633,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1646,7 +1647,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index d66256fc6..37b70e25b 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -1631,6 +1631,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1644,7 +1645,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index dbf0f151e..5ab51fe05 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -1631,6 +1631,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1644,7 +1645,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index d66256fc6..37b70e25b 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -1631,6 +1631,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1644,7 +1645,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
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 d719d58d1..44bf8b012 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -1631,6 +1631,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1644,7 +1645,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index 54193141d..8ead61c0e 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -1620,6 +1620,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1633,7 +1634,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_gid_output.golden b/cli/cmd/testdata/install_gid_output.golden
index 5b16cc2d3..baa7e0ed5 100755
--- a/cli/cmd/testdata/install_gid_output.golden
+++ b/cli/cmd/testdata/install_gid_output.golden
@@ -1638,6 +1638,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1651,7 +1652,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 9d31f777a..25cf99639 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -1764,6 +1764,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1777,7 +1778,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index d713fd2f1..2ad9179a6 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -1764,6 +1764,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1777,7 +1778,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 75bbe4d5c..bca4ca145 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -1562,6 +1562,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1575,7 +1576,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_helm_control_plane_output.golden b/cli/cmd/testdata/install_helm_control_plane_output.golden
index a59741c66..75d874b89 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output.golden
@@ -1606,6 +1606,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1619,7 +1620,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
index ea2d0fef0..467b1382a 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
@@ -1739,6 +1739,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1752,7 +1753,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
index 89fd70713..8bce77649 100755
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha_with_gid.golden
@@ -1746,6 +1746,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1759,7 +1760,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index fd147498b..e3b98933c 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -1751,6 +1751,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1764,7 +1765,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
index ed30e7972..718418ab0 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -1729,6 +1729,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1742,7 +1743,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 7fe206bb5..e7a85011f 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -1624,6 +1624,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1637,7 +1638,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index f16ca02bd..8c04dc728 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -1562,6 +1562,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1575,7 +1576,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
           limits:
             cpu: "cpu-limit"
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index cf7248609..c4ff08aa1 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -1631,6 +1631,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1644,7 +1645,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index aa2647248..d49da99e7 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -1631,6 +1631,7 @@ spec:
           httpGet:
             path: /live
             port: admin-http
+          initialDelaySeconds: 10
         name: policy
         ports:
         - containerPort: 8090
@@ -1644,7 +1645,6 @@ spec:
           httpGet:
             path: /ready
             port: admin-http
-          initialDelaySeconds: 10
         resources:
         securityContext:
           capabilities:
diff --git a/policy-test/Cargo.toml b/policy-test/Cargo.toml
index 2789b5108..82aa16fd4 100644
--- a/policy-test/Cargo.toml
+++ b/policy-test/Cargo.toml
@@ -11,7 +11,7 @@ hyper = { version = "0.14", features = ["client", "http2"] }
 futures = { version = "0.3", default-features = false }
 ipnet = "2"
 k8s-gateway-api = "0.16"
-k8s-openapi = { version = "0.20", features = ["v1_22"] }
+k8s-openapi = { workspace = true }
 linkerd-policy-controller-core = { path = "../policy-controller/core" }
 linkerd-policy-controller-k8s-api = { path = "../policy-controller/k8s/api" }
 linkerd-policy-controller-grpc = { path = "../policy-controller/grpc" }
@@ -26,7 +26,7 @@ tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
 
 [dependencies.kube]
-version = "0.87.1"
+workspace = true
 default-features = false
 features = ["client", "openssl-tls", "runtime", "ws"]
 
diff --git a/policy-test/src/curl.rs b/policy-test/src/curl.rs
index 70480c70f..a12b05614 100644
--- a/policy-test/src/curl.rs
+++ b/policy-test/src/curl.rs
@@ -393,7 +393,7 @@ impl Execable {
         let mut stderr_buf = String::new();
         match stderr.read_to_string(&mut stderr_buf).await {
             Ok(_) => tracing::debug!("curl stderr:\n{stderr_buf}"),
-            Err(error) => tracing::warn!("failed to read curl stderr: {error}"),
+            Err(error) => tracing::warn!("Failed to read curl stderr: {error}"),
         }
         Ok(stdout_buf)
     }
diff --git a/policy-test/src/lib.rs b/policy-test/src/lib.rs
index 0b2b21ae7..cdf2aba7d 100644
--- a/policy-test/src/lib.rs
+++ b/policy-test/src/lib.rs
@@ -8,9 +8,10 @@ pub mod grpc;
 pub mod outbound_api;
 pub mod web;
 
+use kube::runtime::wait::Condition;
 use linkerd_policy_controller_k8s_api::{
     self as k8s,
-    policy::{httproute::ParentReference, EgressNetwork},
+    policy::{httproute::ParentReference, EgressNetwork, TrafficPolicy},
     ResourceExt,
 };
 use maplit::{btreemap, convert_args};
@@ -349,6 +350,26 @@ pub fn endpoints_ready(obj: Option<&k8s::Endpoints>) -> bool {
     false
 }
 
+pub fn egress_network_traffic_policy_is(
+    policy: TrafficPolicy,
+) -> impl Condition<EgressNetwork> + 'static {
+    move |egress_net: Option<&EgressNetwork>| {
+        if let Some(egress_net) = &egress_net {
+            let status = egress_net.status.clone();
+            assert_status_accepted(status.map(|s| s.conditions).unwrap_or_default());
+
+            return egress_net.spec.traffic_policy == policy;
+        }
+        false
+    }
+}
+
+pub fn assert_status_accepted(conditions: Vec<k8s::Condition>) {
+    conditions
+        .iter()
+        .any(|c| c.type_ == "Accepted" && c.status == "True");
+}
+
 #[tracing::instrument(skip_all, fields(%pod, %container))]
 pub async fn logs(client: &kube::Client, ns: &str, pod: &str, container: &str) {
     let params = kube::api::LogParams {
diff --git a/policy-test/tests/e2e_egress_network.rs b/policy-test/tests/e2e_egress_network.rs
new file mode 100644
index 000000000..486bb03a7
--- /dev/null
+++ b/policy-test/tests/e2e_egress_network.rs
@@ -0,0 +1,724 @@
+use k8s::policy::TrafficPolicy;
+use linkerd_policy_controller_k8s_api as k8s;
+use linkerd_policy_test::{
+    assert_status_accepted, await_condition, await_egress_net_status, await_gateway_route_status,
+    await_tcp_route_status, await_tls_route_status, create, create_ready_pod, curl,
+    egress_network_traffic_policy_is, endpoints_ready, update, web, with_temp_ns, LinkerdInject,
+};
+
+#[tokio::test(flavor = "current_thread")]
+async fn default_traffic_policy_http() {
+    with_temp_ns(|client, ns| async move {
+        let mut egress = create(
+            &client,
+            k8s::policy::EgressNetwork {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("egress".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::EgressNetworkSpec {
+                    traffic_policy: k8s::policy::TrafficPolicy::Allow,
+                    networks: None,
+                },
+                status: None,
+            },
+        )
+        .await;
+        let status = await_egress_net_status(&client, &ns, "egress").await;
+        assert_status_accepted(status.conditions);
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let allowed = curl
+            .run(
+                "curl-allowed",
+                "http://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let allowed_status = allowed.http_status_code().await;
+        assert_eq!(allowed_status, 200, "traffic should be allowed");
+
+        // now modify the default traffic policy
+        egress.spec.traffic_policy = k8s::policy::TrafficPolicy::Deny;
+        update(&client, egress).await;
+        await_condition(
+            &client,
+            &ns,
+            "egress",
+            egress_network_traffic_policy_is(TrafficPolicy::Deny),
+        )
+        .await;
+
+        let not_allowed = curl
+            .run(
+                "curl-not-allowed",
+                "http://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let not_allowed_status = not_allowed.http_status_code().await;
+        assert_eq!(not_allowed_status, 403, "traffic should be blocked");
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn default_traffic_policy_opaque() {
+    with_temp_ns(|client, ns| async move {
+        let mut egress = create(
+            &client,
+            k8s::policy::EgressNetwork {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("egress".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::EgressNetworkSpec {
+                    traffic_policy: k8s::policy::TrafficPolicy::Allow,
+                    networks: None,
+                },
+                status: None,
+            },
+        )
+        .await;
+        let status = await_egress_net_status(&client, &ns, "egress").await;
+        assert_status_accepted(status.conditions);
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let allowed = curl
+            .run(
+                "curl-allowed",
+                "https://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let allowed_status = allowed.http_status_code().await;
+        assert_eq!(allowed_status, 200, "traffic should be allowed");
+
+        // now modify the default traffic policy
+        egress.spec.traffic_policy = k8s::policy::TrafficPolicy::Deny;
+        update(&client, egress).await;
+        await_condition(
+            &client,
+            &ns,
+            "egress",
+            egress_network_traffic_policy_is(TrafficPolicy::Deny),
+        )
+        .await;
+
+        let not_allowed = curl
+            .run(
+                "curl-not-allowed",
+                "https://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let not_allowed_exit_code = not_allowed.exit_code().await;
+        assert_ne!(not_allowed_exit_code, 0, "traffic should be blocked");
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn explicit_allow_http_route() {
+    with_temp_ns(|client, ns| async move {
+        create(
+            &client,
+            k8s::policy::EgressNetwork {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("egress".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::EgressNetworkSpec {
+                    traffic_policy: k8s::policy::TrafficPolicy::Deny,
+                    networks: None,
+                },
+                status: None,
+            },
+        )
+        .await;
+        let status = await_egress_net_status(&client, &ns, "egress").await;
+        assert_status_accepted(status.conditions);
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let not_allowed_get = curl
+            .run(
+                "curl-not-allowed-get",
+                "http://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let not_allowed_get_status = not_allowed_get.http_status_code().await;
+        assert_eq!(not_allowed_get_status, 403, "traffic should be blocked");
+
+        // Now create an http route that will allow explicit hostname and explicit path
+        create(
+            &client,
+            k8s::gateway::HttpRoute {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("http-route".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::gateway::HttpRouteSpec {
+                    inner: k8s::gateway::CommonRouteSpec {
+                        parent_refs: Some(vec![k8s::policy::httproute::ParentReference {
+                            namespace: None,
+                            name: "egress".to_string(),
+                            port: Some(80),
+                            group: Some("policy.linkerd.io".to_string()),
+                            kind: Some("EgressNetwork".to_string()),
+                            section_name: None,
+                        }]),
+                    },
+                    hostnames: None,
+                    rules: Some(vec![k8s::gateway::HttpRouteRule {
+                        matches: Some(vec![k8s::policy::httproute::HttpRouteMatch {
+                            path: Some(k8s::policy::httproute::HttpPathMatch::Exact {
+                                value: "/get".to_string(),
+                            }),
+                            ..Default::default()
+                        }]),
+                        backend_refs: None,
+                        filters: None,
+                    }]),
+                },
+                status: None,
+            },
+        )
+        .await;
+        await_gateway_route_status(&client, &ns, "http-route").await;
+
+        // traffic should be allowed for /get request
+        let allowed_get = curl
+            .run(
+                "curl-allowed-get",
+                "http://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let allowed_get_status = allowed_get.http_status_code().await;
+        assert_eq!(allowed_get_status, 200, "traffic should be allowed");
+
+        // traffic should not be allowed for /ip request
+        let not_allowed_ip = curl
+            .run(
+                "curl-not-allowed-ip",
+                "http://httpbin.org/ip",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let not_allowed_ip_status = not_allowed_ip.http_status_code().await;
+        assert_eq!(not_allowed_ip_status, 403, "traffic should not be allowed");
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn explicit_allow_tls_route() {
+    with_temp_ns(|client, ns| async move {
+        create(
+            &client,
+            k8s::policy::EgressNetwork {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("egress".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::EgressNetworkSpec {
+                    traffic_policy: k8s::policy::TrafficPolicy::Deny,
+                    networks: None,
+                },
+                status: None,
+            },
+        )
+        .await;
+        let status = await_egress_net_status(&client, &ns, "egress").await;
+        assert_status_accepted(status.conditions);
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let not_allowed_httpbin = curl
+            .run(
+                "not-allowed-httpbin",
+                "https://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let not_allowed_httpbin_exit_code = not_allowed_httpbin.exit_code().await;
+        assert_ne!(
+            not_allowed_httpbin_exit_code, 0,
+            "traffic should be blocked"
+        );
+
+        // Now create a tls route that will allow explicit hostname and explicit path
+        create(
+            &client,
+            k8s_gateway_api::TlsRoute {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("tls-route".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s_gateway_api::TlsRouteSpec {
+                    inner: k8s_gateway_api::CommonRouteSpec {
+                        parent_refs: Some(vec![k8s_gateway_api::ParentReference {
+                            namespace: None,
+                            name: "egress".to_string(),
+                            port: Some(443),
+                            group: Some("policy.linkerd.io".to_string()),
+                            kind: Some("EgressNetwork".to_string()),
+                            section_name: None,
+                        }]),
+                    },
+                    hostnames: Some(vec!["httpbin.org".to_string()]),
+                    rules: vec![k8s_gateway_api::TlsRouteRule {
+                        backend_refs: vec![k8s_gateway_api::BackendRef {
+                            weight: None,
+                            inner: k8s_gateway_api::BackendObjectReference {
+                                namespace: None,
+                                name: "egress".to_string(),
+                                port: Some(443),
+                                group: Some("policy.linkerd.io".to_string()),
+                                kind: Some("EgressNetwork".to_string()),
+                            },
+                        }],
+                    }],
+                },
+                status: None,
+            },
+        )
+        .await;
+        await_tls_route_status(&client, &ns, "tls-route").await;
+
+        // traffic should be allowed for httpbin
+        let allowed_httpbin = curl
+            .run(
+                "allowed-httpbin",
+                "https://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let allowed_httpbin_status = allowed_httpbin.http_status_code().await;
+        assert_eq!(allowed_httpbin_status, 200, "traffic should be allowed");
+
+        // traffic should not be allowed for google.com
+        let not_allowed_google = curl
+            .run(
+                "curl-not-allowed-google",
+                "https://google.com/",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let not_allowed_google_exit_code = not_allowed_google.exit_code().await;
+        assert_ne!(
+            not_allowed_google_exit_code, 0,
+            "traffic should not be allowed"
+        );
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn explicit_allow_tcp_route() {
+    with_temp_ns(|client, ns| async move {
+        create(
+            &client,
+            k8s::policy::EgressNetwork {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("egress".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::EgressNetworkSpec {
+                    traffic_policy: k8s::policy::TrafficPolicy::Deny,
+                    networks: None,
+                },
+                status: None,
+            },
+        )
+        .await;
+        let status = await_egress_net_status(&client, &ns, "egress").await;
+        assert_status_accepted(status.conditions);
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let not_allowed_httpbin = curl
+            .run(
+                "not-allowed-httpbin",
+                "https://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let not_allowed_httpbin_exit_code = not_allowed_httpbin.exit_code().await;
+        assert_ne!(
+            not_allowed_httpbin_exit_code, 0,
+            "traffic should be blocked"
+        );
+
+        // Now create a tcp route that will allow explicit hostname and explicit path
+        create(
+            &client,
+            k8s_gateway_api::TcpRoute {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("tcp-route".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s_gateway_api::TcpRouteSpec {
+                    inner: k8s_gateway_api::CommonRouteSpec {
+                        parent_refs: Some(vec![k8s_gateway_api::ParentReference {
+                            namespace: None,
+                            name: "egress".to_string(),
+                            port: Some(443),
+                            group: Some("policy.linkerd.io".to_string()),
+                            kind: Some("EgressNetwork".to_string()),
+                            section_name: None,
+                        }]),
+                    },
+                    rules: vec![k8s_gateway_api::TcpRouteRule {
+                        backend_refs: vec![k8s_gateway_api::BackendRef {
+                            weight: None,
+                            inner: k8s_gateway_api::BackendObjectReference {
+                                namespace: None,
+                                name: "egress".to_string(),
+                                port: Some(443),
+                                group: Some("policy.linkerd.io".to_string()),
+                                kind: Some("EgressNetwork".to_string()),
+                            },
+                        }],
+                    }],
+                },
+                status: None,
+            },
+        )
+        .await;
+        await_tcp_route_status(&client, &ns, "tcp-route").await;
+
+        // traffic should be allowed for httpbin on 443
+        let allowed_httpbin = curl
+            .run(
+                "allowed-httpbin",
+                "https://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let allowed_httpbin_status = allowed_httpbin.http_status_code().await;
+        assert_eq!(allowed_httpbin_status, 200, "traffic should be allowed");
+
+        // traffic should not be allowed for httpbin on 80
+        let not_allowed_google = curl
+            .run(
+                "curl-not-allowed-google",
+                "http://google.com:80/",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let not_allowed_google_status = not_allowed_google.http_status_code().await;
+        assert_eq!(
+            not_allowed_google_status, 403,
+            "traffic should not be allowed"
+        );
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn routing_back_to_cluster_http_route() {
+    with_temp_ns(|client, ns| async move {
+        create(
+            &client,
+            k8s::policy::EgressNetwork {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("egress".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::EgressNetworkSpec {
+                    traffic_policy: k8s::policy::TrafficPolicy::Allow,
+                    networks: None,
+                },
+                status: None,
+            },
+        )
+        .await;
+        let status = await_egress_net_status(&client, &ns, "egress").await;
+        assert_status_accepted(status.conditions);
+
+        // Create the web pod and wait for it to be ready.
+        tokio::join!(
+            create(&client, web::service(&ns)),
+            create_ready_pod(&client, web::pod(&ns))
+        );
+
+        await_condition(&client, &ns, "web", endpoints_ready).await;
+
+        // Now create an http route that will route requests
+        // back to the cluster if the request path is /get
+        // and will let the rest go through
+        create(
+            &client,
+            k8s::gateway::HttpRoute {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("http-route".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::gateway::HttpRouteSpec {
+                    inner: k8s_gateway_api::CommonRouteSpec {
+                        parent_refs: Some(vec![k8s_gateway_api::ParentReference {
+                            namespace: None,
+                            name: "egress".to_string(),
+                            port: Some(80),
+                            group: Some("policy.linkerd.io".to_string()),
+                            kind: Some("EgressNetwork".to_string()),
+                            section_name: None,
+                        }]),
+                    },
+                    hostnames: Some(vec!["httpbin.org".to_string()]),
+                    rules: Some(vec![k8s::gateway::HttpRouteRule {
+                        matches: Some(vec![k8s_gateway_api::HttpRouteMatch {
+                            path: Some(k8s_gateway_api::HttpPathMatch::Exact {
+                                value: "/get".to_string(),
+                            }),
+                            ..Default::default()
+                        }]),
+                        backend_refs: Some(vec![k8s_gateway_api::HttpBackendRef {
+                            backend_ref: Some(k8s_gateway_api::BackendRef {
+                                weight: None,
+                                inner: k8s_gateway_api::BackendObjectReference {
+                                    namespace: Some(ns.clone()),
+                                    name: "web".to_string(),
+                                    port: Some(80),
+                                    group: None,
+                                    kind: None,
+                                },
+                            }),
+                            filters: None,
+                        }]),
+                        filters: None,
+                    }]),
+                },
+                status: None,
+            },
+        )
+        .await;
+        await_gateway_route_status(&client, &ns, "http-route").await;
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let (in_cluster, out_of_cluster) = tokio::join!(
+            curl.run(
+                "curl-in-cluster",
+                "http://httpbin.org/get",
+                LinkerdInject::Enabled
+            ),
+            curl.run(
+                "curl-out-of-cluster",
+                "http://httpbin.org/ip",
+                LinkerdInject::Enabled
+            ),
+        );
+
+        let (in_cluster_status, out_of_cluster_status) = tokio::join!(
+            in_cluster.http_status_code(),
+            out_of_cluster.http_status_code(),
+        );
+
+        assert_eq!(in_cluster_status, 204); // in-cluster service returns 204
+        assert_eq!(out_of_cluster_status, 200); // external service returns 200
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn routing_back_to_cluster_tls_route() {
+    with_temp_ns(|client, ns| async move {
+        create(
+            &client,
+            k8s::policy::EgressNetwork {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("egress".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::EgressNetworkSpec {
+                    traffic_policy: k8s::policy::TrafficPolicy::Allow,
+                    networks: None,
+                },
+                status: None,
+            },
+        )
+        .await;
+        let status = await_egress_net_status(&client, &ns, "egress").await;
+        assert_status_accepted(status.conditions);
+
+        // Create the web pod and wait for it to be ready.
+        tokio::join!(
+            create(&client, web::service(&ns)),
+            create_ready_pod(&client, web::pod(&ns))
+        );
+
+        await_condition(&client, &ns, "web", endpoints_ready).await;
+
+        // Now create an tls route that will route requests
+        // to an in-cluster service based on SNI
+        create(
+            &client,
+            k8s_gateway_api::TlsRoute {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("tls-route".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s_gateway_api::TlsRouteSpec {
+                    inner: k8s_gateway_api::CommonRouteSpec {
+                        parent_refs: Some(vec![k8s::policy::httproute::ParentReference {
+                            namespace: None,
+                            name: "egress".to_string(),
+                            port: Some(443),
+                            group: Some("policy.linkerd.io".to_string()),
+                            kind: Some("EgressNetwork".to_string()),
+                            section_name: None,
+                        }]),
+                    },
+                    hostnames: Some(vec!["httpbin.org".to_string()]),
+                    rules: vec![k8s_gateway_api::TlsRouteRule {
+                        backend_refs: vec![k8s::gateway::BackendRef {
+                            weight: None,
+                            inner: k8s_gateway_api::BackendObjectReference {
+                                namespace: Some(ns.clone()),
+                                name: "web".to_string(),
+                                port: Some(80),
+                                group: None,
+                                kind: None,
+                            },
+                        }],
+                    }],
+                },
+                status: None,
+            },
+        )
+        .await;
+        await_tls_route_status(&client, &ns, "tls-route").await;
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let (in_cluster, out_of_cluster) = tokio::join!(
+            curl.run(
+                "curl-in-cluster",
+                "https://httpbin.org/get",
+                LinkerdInject::Enabled
+            ),
+            curl.run(
+                "curl-out-of-cluster",
+                "https://google.com/not-there",
+                LinkerdInject::Enabled
+            ),
+        );
+
+        let (in_cluster_exit_code, out_of_cluster_status) =
+            tokio::join!(in_cluster.exit_code(), out_of_cluster.http_status_code(),);
+
+        assert_ne!(in_cluster_exit_code, 0); // in-cluster service fails because it does not expect TLS
+        assert_eq!(out_of_cluster_status, 404); // external service returns 404 as this path does not exist
+    })
+    .await;
+}
+
+#[tokio::test(flavor = "current_thread")]
+async fn routing_back_to_cluster_tcp_route() {
+    with_temp_ns(|client, ns| async move {
+        create(
+            &client,
+            k8s::policy::EgressNetwork {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("egress".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s::policy::EgressNetworkSpec {
+                    traffic_policy: k8s::policy::TrafficPolicy::Allow,
+                    networks: None,
+                },
+                status: None,
+            },
+        )
+        .await;
+        let status = await_egress_net_status(&client, &ns, "egress").await;
+        assert_status_accepted(status.conditions);
+
+        // Create the web pod and wait for it to be ready.
+        tokio::join!(
+            create(&client, web::service(&ns)),
+            create_ready_pod(&client, web::pod(&ns))
+        );
+
+        await_condition(&client, &ns, "web", endpoints_ready).await;
+
+        // Now create an tls route that will route requests
+        // to an in-cluster service based on SNI
+        create(
+            &client,
+            k8s_gateway_api::TcpRoute {
+                metadata: k8s::ObjectMeta {
+                    namespace: Some(ns.clone()),
+                    name: Some("tcp-route".to_string()),
+                    ..Default::default()
+                },
+                spec: k8s_gateway_api::TcpRouteSpec {
+                    inner: k8s_gateway_api::CommonRouteSpec {
+                        parent_refs: Some(vec![k8s_gateway_api::ParentReference {
+                            namespace: None,
+                            name: "egress".to_string(),
+                            port: Some(80),
+                            group: Some("policy.linkerd.io".to_string()),
+                            kind: Some("EgressNetwork".to_string()),
+                            section_name: None,
+                        }]),
+                    },
+                    rules: vec![k8s_gateway_api::TcpRouteRule {
+                        backend_refs: vec![k8s::gateway::BackendRef {
+                            weight: None,
+                            inner: k8s_gateway_api::BackendObjectReference {
+                                namespace: Some(ns.clone()),
+                                name: "web".to_string(),
+                                port: Some(80),
+                                group: None,
+                                kind: None,
+                            },
+                        }],
+                    }],
+                },
+                status: None,
+            },
+        )
+        .await;
+        await_tcp_route_status(&client, &ns, "tcp-route").await;
+
+        let curl = curl::Runner::init(&client, &ns).await;
+        let in_cluster = curl
+            .run(
+                "curl-in-cluster",
+                "http://httpbin.org/get",
+                LinkerdInject::Enabled,
+            )
+            .await;
+
+        let in_cluster_status = in_cluster.http_status_code().await;
+
+        assert_eq!(in_cluster_status, 204); // in-cluster service returns 204
+    })
+    .await;
+}
diff --git a/policy-test/tests/outbound_api_egress_network.rs b/policy-test/tests/outbound_api_egress_network.rs
index 99f468274..b8e75a5a1 100644
--- a/policy-test/tests/outbound_api_egress_network.rs
+++ b/policy-test/tests/outbound_api_egress_network.rs
@@ -1,14 +1,16 @@
 use futures::prelude::*;
 use linkerd2_proxy_api::meta;
 use linkerd_policy_test::{
-    await_egress_net_status, create_egress_network, delete, grpc, with_temp_ns,
+    assert_status_accepted, await_egress_net_status, create_egress_network, delete, grpc,
+    with_temp_ns,
 };
 
 #[tokio::test(flavor = "current_thread")]
 async fn egress_switches_to_fallback() {
     with_temp_ns(|client, ns| async move {
         let egress_net = create_egress_network(&client, &ns, "egress-net").await;
-        await_egress_net_status(&client, &ns, "egress-net").await;
+        let status = await_egress_net_status(&client, &ns, "egress-net").await;
+        assert_status_accepted(status.conditions);
 
         let mut policy_api = grpc::OutboundPolicyClient::port_forwarded(&client).await;
         let mut rsp = policy_api.watch_ip(&ns, "1.1.1.1", 80).await.unwrap();
@@ -58,7 +60,8 @@ async fn fallback_switches_to_egress() {
         assert_eq!(meta, expected_meta);
 
         let _egress_net = create_egress_network(&client, &ns, "egress-net").await;
-        await_egress_net_status(&client, &ns, "egress-net").await;
+        let status = await_egress_net_status(&client, &ns, "egress-net").await;
+        assert_status_accepted(status.conditions);
 
         // stream should fall apart now
         assert!(rsp.next().await.is_none());
diff --git a/policy-test/tests/outbound_api_gateway.rs b/policy-test/tests/outbound_api_gateway.rs
index e466a8308..1e96c40f9 100644
--- a/policy-test/tests/outbound_api_gateway.rs
+++ b/policy-test/tests/outbound_api_gateway.rs
@@ -2,11 +2,11 @@ use futures::prelude::*;
 use kube::ResourceExt;
 use linkerd_policy_controller_k8s_api as k8s;
 use linkerd_policy_test::{
-    assert_default_accrual_backoff, assert_resource_meta, await_egress_net_status,
-    await_gateway_route_status, create, create_annotated_egress_network, create_annotated_service,
-    create_cluster_scoped, create_egress_network, create_opaque_egress_network,
-    create_opaque_service, create_service, delete_cluster_scoped, grpc, mk_egress_net, mk_service,
-    outbound_api::*, update, with_temp_ns, Resource,
+    assert_default_accrual_backoff, assert_resource_meta, assert_status_accepted,
+    await_egress_net_status, await_gateway_route_status, create, create_annotated_egress_network,
+    create_annotated_service, create_cluster_scoped, create_egress_network,
+    create_opaque_egress_network, create_opaque_service, create_service, delete_cluster_scoped,
+    grpc, mk_egress_net, mk_service, outbound_api::*, update, with_temp_ns, Resource,
 };
 use maplit::{btreemap, convert_args};
 use std::{collections::BTreeMap, time::Duration};
@@ -48,7 +48,8 @@ async fn egress_net_with_no_http_routes() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_no_http_routes(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -70,7 +71,8 @@ async fn egress_net_with_http_route_without_rules() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_http_route_without_rules(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -92,7 +94,8 @@ async fn egress_net_with_http_routes_without_backends() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_http_routes_without_backends(Resource::EgressNetwork(egress), &client, &ns)
             .await;
@@ -122,7 +125,8 @@ async fn egress_net_with_http_routes_with_backend() {
     with_temp_ns(|client, ns| async move {
         // Create a service
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_http_routes_with_backend(
             Resource::EgressNetwork(egress.clone()),
@@ -233,7 +237,8 @@ async fn egress_net_with_http_routes_with_invalid_backend() {
     with_temp_ns(|client, ns| async move {
         // Create an egress network
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         let backend = mk_egress_net(&ns, "invalid");
 
@@ -267,7 +272,8 @@ async fn egress_net_with_multiple_http_routes() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_multiple_http_routes(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -342,7 +348,8 @@ async fn egress_net_with_consecutive_failure_accrual() {
             ]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "consecutive-accrual-egress").await;
+        let status = await_egress_net_status(&client, &ns, "consecutive-accrual-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_consecutive_failure_accrual(Resource::EgressNetwork(egress), &client, &ns)
             .await;
@@ -458,7 +465,8 @@ async fn egress_net_with_consecutive_failure_accrual_defaults_no_config() {
             )]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "default-accrual-egress").await;
+        let status = await_egress_net_status(&client, &ns, "default-accrual-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_consecutive_failure_accrual_defaults_no_config(
             Resource::EgressNetwork(egress_no_config),
@@ -491,7 +499,8 @@ async fn egress_net_with_consecutive_failure_accrual_defaults_max_fails() {
             ]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "no-backoff-egress").await;
+        let status = await_egress_net_status(&client, &ns, "no-backoff-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_consecutive_failure_accrual_defaults_max_fails(
             Resource::EgressNetwork(egress_max_fails),
@@ -524,7 +533,8 @@ async fn egress_net_with_consecutive_failure_accrual_defaults_jitter() {
             ]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "only-jitter-egress").await;
+        let status = await_egress_net_status(&client, &ns, "only-jitter-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_consecutive_failure_accrual_defaults_max_jitter(
             Resource::EgressNetwork(egress_jitter),
@@ -572,7 +582,8 @@ async fn egress_net_with_default_failure_accrual() {
     with_temp_ns(|client, ns| async move {
         // Default config for EgressNetwork, no failure accrual
         let egress_default = create_egress_network(&client, &ns, "default-failure-accrual").await;
-        await_egress_net_status(&client, &ns, "default-failure-accrual").await;
+        let status = await_egress_net_status(&client, &ns, "default-failure-accrual").await;
+        assert_status_accepted(status.conditions);
 
         // Create EgressNetwork with consecutive failure accrual config for
         // max_failures but no mode
@@ -586,7 +597,8 @@ async fn egress_net_with_default_failure_accrual() {
             )]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "default-max-failure-egress").await;
+        let status = await_egress_net_status(&client, &ns, "default-max-failure-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_default_failure_accrual(
             Resource::EgressNetwork(egress_default),
@@ -642,7 +654,8 @@ async fn route_with_filters_egress_net() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         route_with_filters(
             Resource::EgressNetwork(egress.clone()),
@@ -677,7 +690,8 @@ async fn backend_with_filters_egress_net() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         backend_with_filters(
             Resource::EgressNetwork(egress.clone()),
@@ -987,7 +1001,8 @@ async fn http_route_retries_and_timeouts_egress_net() {
     with_temp_ns(|client, ns| async move {
         // Create an egress network
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         http_route_retries_and_timeouts(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -1022,7 +1037,8 @@ async fn egress_net_retries_and_timeouts() {
             .annotations_mut()
             .insert("timeout.linkerd.io/response".to_string(), "10s".to_string());
         let egress = Resource::EgressNetwork(create(&client, egress).await);
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         retries_and_timeouts(egress, &client, &ns).await;
     })
@@ -1044,7 +1060,8 @@ async fn egress_net_http_route_reattachment() {
     with_temp_ns(|client, ns| async move {
         // Create a egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         http_route_reattachment(Resource::EgressNetwork(egress), &client, &ns).await;
     })
diff --git a/policy-test/tests/outbound_api_grpc.rs b/policy-test/tests/outbound_api_grpc.rs
index 5f31f30d1..e2493694f 100644
--- a/policy-test/tests/outbound_api_grpc.rs
+++ b/policy-test/tests/outbound_api_grpc.rs
@@ -1,9 +1,9 @@
 use futures::prelude::*;
 use kube::ResourceExt;
 use linkerd_policy_test::{
-    assert_resource_meta, await_egress_net_status, await_grpc_route_status, create,
-    create_egress_network, create_service, mk_egress_net, mk_service, outbound_api::*, update,
-    with_temp_ns, Resource,
+    assert_resource_meta, assert_status_accepted, await_egress_net_status, await_grpc_route_status,
+    create, create_egress_network, create_service, mk_egress_net, mk_service, outbound_api::*,
+    update, with_temp_ns, Resource,
 };
 use std::collections::BTreeMap;
 
@@ -23,7 +23,8 @@ async fn egress_net_grpc_route_retries_and_timeouts() {
         // Create a egress net
         let egress =
             Resource::EgressNetwork(create_egress_network(&client, &ns, "my-egress").await);
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         grpc_route_retries_and_timeouts(egress, &client, &ns).await;
     })
@@ -58,7 +59,8 @@ async fn egress_net_retries_and_timeouts() {
             .annotations_mut()
             .insert("timeout.linkerd.io/response".to_string(), "10s".to_string());
         let egress = Resource::EgressNetwork(create(&client, egress).await);
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_retries_and_timeouts(egress, &client, &ns).await;
     })
@@ -80,7 +82,8 @@ async fn egress_net_grpc_route_reattachment() {
     with_temp_ns(|client, ns| async move {
         // Create a egress network
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         grpc_route_reattachment(Resource::EgressNetwork(egress), &client, &ns).await;
     })
diff --git a/policy-test/tests/outbound_api_linkerd.rs b/policy-test/tests/outbound_api_linkerd.rs
index 93e533b48..4e1619c8b 100644
--- a/policy-test/tests/outbound_api_linkerd.rs
+++ b/policy-test/tests/outbound_api_linkerd.rs
@@ -4,11 +4,11 @@ use futures::prelude::*;
 use kube::ResourceExt;
 use linkerd_policy_controller_k8s_api as k8s;
 use linkerd_policy_test::{
-    assert_default_accrual_backoff, assert_resource_meta, await_egress_net_status,
-    await_route_status, create, create_annotated_egress_network, create_annotated_service,
-    create_cluster_scoped, create_egress_network, create_opaque_egress_network,
-    create_opaque_service, create_service, delete_cluster_scoped, grpc, mk_egress_net, mk_service,
-    outbound_api::*, update, with_temp_ns, Resource,
+    assert_default_accrual_backoff, assert_resource_meta, assert_status_accepted,
+    await_egress_net_status, await_route_status, create, create_annotated_egress_network,
+    create_annotated_service, create_cluster_scoped, create_egress_network,
+    create_opaque_egress_network, create_opaque_service, create_service, delete_cluster_scoped,
+    grpc, mk_egress_net, mk_service, outbound_api::*, update, with_temp_ns, Resource,
 };
 use maplit::{btreemap, convert_args};
 
@@ -49,7 +49,8 @@ async fn egress_net_with_no_http_routes() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_no_http_routes(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -71,7 +72,8 @@ async fn egress_net_with_http_route_without_rules() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_http_route_without_rules(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -93,7 +95,8 @@ async fn egress_net_with_http_routes_without_backends() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_http_routes_without_backends(Resource::EgressNetwork(egress), &client, &ns)
             .await;
@@ -123,7 +126,8 @@ async fn egress_net_with_http_routes_with_backend() {
     with_temp_ns(|client, ns| async move {
         // Create a service
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_http_routes_with_backend(
             Resource::EgressNetwork(egress.clone()),
@@ -234,7 +238,8 @@ async fn egress_net_with_http_routes_with_invalid_backend() {
     with_temp_ns(|client, ns| async move {
         // Create an egress network
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         let backend = mk_egress_net(&ns, "invalid");
 
@@ -268,7 +273,8 @@ async fn egress_net_with_multiple_http_routes() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_multiple_http_routes(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -343,7 +349,8 @@ async fn egress_net_with_consecutive_failure_accrual() {
             ]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "consecutive-accrual-egress").await;
+        let status = await_egress_net_status(&client, &ns, "consecutive-accrual-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_consecutive_failure_accrual(Resource::EgressNetwork(egress), &client, &ns)
             .await;
@@ -459,7 +466,8 @@ async fn egress_net_with_consecutive_failure_accrual_defaults_no_config() {
             )]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "default-accrual-egress").await;
+        let status = await_egress_net_status(&client, &ns, "default-accrual-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_consecutive_failure_accrual_defaults_no_config(
             Resource::EgressNetwork(egress_no_config),
@@ -492,7 +500,8 @@ async fn egress_net_with_consecutive_failure_accrual_defaults_max_fails() {
             ]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "no-backoff-egress").await;
+        let status = await_egress_net_status(&client, &ns, "no-backoff-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_consecutive_failure_accrual_defaults_max_fails(
             Resource::EgressNetwork(egress_max_fails),
@@ -525,7 +534,8 @@ async fn egress_net_with_consecutive_failure_accrual_defaults_jitter() {
             ]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "only-jitter-egress").await;
+        let status = await_egress_net_status(&client, &ns, "only-jitter-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_consecutive_failure_accrual_defaults_max_jitter(
             Resource::EgressNetwork(egress_jitter),
@@ -573,7 +583,8 @@ async fn egress_net_with_default_failure_accrual() {
     with_temp_ns(|client, ns| async move {
         // Default config for EgressNetwork, no failure accrual
         let egress_default = create_egress_network(&client, &ns, "default-failure-accrual").await;
-        await_egress_net_status(&client, &ns, "default-failure-accrual").await;
+        let status = await_egress_net_status(&client, &ns, "default-failure-accrual").await;
+        assert_status_accepted(status.conditions);
 
         // Create EgressNetwork with consecutive failure accrual config for
         // max_failures but no mode
@@ -587,7 +598,8 @@ async fn egress_net_with_default_failure_accrual() {
             )]),
         )
         .await;
-        await_egress_net_status(&client, &ns, "default-max-failure-egress").await;
+        let status = await_egress_net_status(&client, &ns, "default-max-failure-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_default_failure_accrual(
             Resource::EgressNetwork(egress_default),
@@ -643,7 +655,8 @@ async fn route_with_filters_egress_net() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         route_with_filters(
             Resource::EgressNetwork(egress.clone()),
@@ -678,7 +691,8 @@ async fn backend_with_filters_egress_net() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         backend_with_filters(
             Resource::EgressNetwork(egress.clone()),
@@ -988,7 +1002,8 @@ async fn http_route_retries_and_timeouts_egress_net() {
     with_temp_ns(|client, ns| async move {
         // Create an egress network
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         http_route_retries_and_timeouts(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -1023,7 +1038,8 @@ async fn egress_net_retries_and_timeouts() {
             .annotations_mut()
             .insert("timeout.linkerd.io/response".to_string(), "10s".to_string());
         let egress = Resource::EgressNetwork(create(&client, egress).await);
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         retries_and_timeouts(egress, &client, &ns).await;
     })
@@ -1045,7 +1061,8 @@ async fn egress_net_http_route_reattachment() {
     with_temp_ns(|client, ns| async move {
         // Create a egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         http_route_reattachment(Resource::EgressNetwork(egress), &client, &ns).await;
     })
diff --git a/policy-test/tests/outbound_api_tcp.rs b/policy-test/tests/outbound_api_tcp.rs
index 52db12b6e..7749a6fa9 100644
--- a/policy-test/tests/outbound_api_tcp.rs
+++ b/policy-test/tests/outbound_api_tcp.rs
@@ -1,8 +1,8 @@
 use futures::prelude::*;
 use linkerd_policy_controller_k8s_api as k8s;
 use linkerd_policy_test::{
-    assert_resource_meta, await_egress_net_status, await_tcp_route_status, create,
-    create_cluster_scoped, create_egress_network, create_service, delete_cluster_scoped,
+    assert_resource_meta, assert_status_accepted, await_egress_net_status, await_tcp_route_status,
+    create, create_cluster_scoped, create_egress_network, create_service, delete_cluster_scoped,
     mk_egress_net, mk_service, outbound_api::*, update, with_temp_ns, Resource,
 };
 use maplit::{btreemap, convert_args};
@@ -29,7 +29,8 @@ async fn egress_net_with_tcp_routes_with_backend() {
     with_temp_ns(|client, ns| async move {
         // Create a service
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_tcp_routes_with_backend(
             Resource::EgressNetwork(egress.clone()),
@@ -115,7 +116,8 @@ async fn egress_net_with_tcp_routes_with_invalid_backend() {
     with_temp_ns(|client, ns| async move {
         // Create an egress network
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         let backend = mk_egress_net(&ns, "invalid");
 
@@ -145,7 +147,8 @@ async fn egress_net_with_multiple_tcp_routes() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_multiple_tcp_routes(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -390,7 +393,8 @@ async fn egress_net_tcp_route_reattachment() {
     with_temp_ns(|client, ns| async move {
         // Create a egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         tcp_route_reattachment(Resource::EgressNetwork(egress), &client, &ns).await;
     })
diff --git a/policy-test/tests/outbound_api_tls.rs b/policy-test/tests/outbound_api_tls.rs
index b2fd4d015..cf7569dd1 100644
--- a/policy-test/tests/outbound_api_tls.rs
+++ b/policy-test/tests/outbound_api_tls.rs
@@ -1,9 +1,9 @@
 use futures::prelude::*;
 use linkerd_policy_controller_k8s_api as k8s;
 use linkerd_policy_test::{
-    assert_resource_meta, await_egress_net_status, await_tls_route_status, create,
-    create_cluster_scoped, create_egress_network, create_service, delete_cluster_scoped, grpc,
-    mk_egress_net, mk_service, outbound_api::*, update, with_temp_ns, Resource,
+    assert_resource_meta, assert_status_accepted, await_egress_net_status, await_tls_route_status,
+    create, create_cluster_scoped, create_egress_network, create_service, delete_cluster_scoped,
+    grpc, mk_egress_net, mk_service, outbound_api::*, update, with_temp_ns, Resource,
 };
 use maplit::{btreemap, convert_args};
 
@@ -29,7 +29,8 @@ async fn egress_net_with_tls_routes_with_backend() {
     with_temp_ns(|client, ns| async move {
         // Create a service
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_tls_routes_with_backend(
             Resource::EgressNetwork(egress.clone()),
@@ -115,7 +116,8 @@ async fn egress_net_with_tls_routes_with_invalid_backend() {
     with_temp_ns(|client, ns| async move {
         // Create an egress network
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         let backend = mk_egress_net(&ns, "invalid");
 
@@ -145,7 +147,8 @@ async fn egress_net_with_multiple_http_routes() {
     with_temp_ns(|client, ns| async move {
         // Create an egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         parent_with_multiple_tls_routes(Resource::EgressNetwork(egress), &client, &ns).await;
     })
@@ -390,7 +393,8 @@ async fn egress_net_tls_route_reattachment() {
     with_temp_ns(|client, ns| async move {
         // Create a egress net
         let egress = create_egress_network(&client, &ns, "my-egress").await;
-        await_egress_net_status(&client, &ns, "my-egress").await;
+        let status = await_egress_net_status(&client, &ns, "my-egress").await;
+        assert_status_accepted(status.conditions);
 
         tls_route_reattachment(Resource::EgressNetwork(egress), &client, &ns).await;
     })
diff --git a/test/integration/multicluster/multicluster-traffic/federated_test.go b/test/integration/multicluster/multicluster-traffic/federated_test.go
index 6c39526a7..bfb5603c7 100644
--- a/test/integration/multicluster/multicluster-traffic/federated_test.go
+++ b/test/integration/multicluster/multicluster-traffic/federated_test.go
@@ -81,7 +81,7 @@ func TestFederatedService(t *testing.T) {
 
 		})
 
-		timeout := time.Minute
+		timeout := 3 * time.Minute
 		t.Run("Ensure federated service exists and has no endpoints", func(t *testing.T) {
 			err := TestHelper.SwitchContext(contexts[testutil.SourceContextKey])
 			if err != nil {