Repository: linkerd/linkerd2
Pull request #13314 (2024-11-12 23:23:00)

Implement status handling for HTTPLocalRateLimitPolicy CRD
This adds the status stanza to the HTTPLocalRateLimitPolicy CRD, and implements its handling in the policy status controller.

For the controller to accept an HTTPLocalRateLimitPolicy CR it checks that:
- The targetRef is an existing Server
- If there are multiple HTTPLocalRateLimitPolicy CRs pointing to the same server, only accept the oldest one, or if created at the same time, the first in alphabetical order (that logic was moved from the inbound indexer to the status controller).

6cd7dc22c: Update RL CRD and RBAC to allow patching its status
69aee0129: Update golden files
60f25b716: Implement status handling for HTTPLocalRateLimitPolicy CRD
fc99d3adf: Update existing unit tests
0204acf65: New unit tests

## Examples

Not accepted CR:
```yaml
...
status:
  conditions:
  - lastTransitionTime: "2024-11-12T23:10:05Z"
    message: ""
    reason: RateLimitReasonAlreadyExists
    status: "False"
    type: Accepted
  targetRef:
    group: policy.linkerd.io
    kind: Server
    name: web-http
```

Accepted CR:
```yaml
...
status:
  conditions:
  - lastTransitionTime: "2024-11-12T23:10:05Z"
    message: ""
    reason: Accepted
    status: "True"
    type: Accepted
  targetRef:
    group: policy.linkerd.io
    kind: Server
    name: web-http
```


Patch:
diff --git a/.proxy-version b/.proxy-version
index 723c8740f..c5c4cea7b 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.263.1
+v2.265.0
diff --git a/Cargo.lock b/Cargo.lock
index c950f3879..f54c6fb3f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -326,9 +326,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.14"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
+checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6"
 dependencies = [
  "libc",
 ]
@@ -1997,9 +1997,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.214"
+version = "1.0.215"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
+checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
 dependencies = [
  "serde_derive",
 ]
@@ -2016,9 +2016,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.214"
+version = "1.0.215"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
+checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/charts/linkerd-control-plane/templates/destination-rbac.yaml b/charts/linkerd-control-plane/templates/destination-rbac.yaml
index e53608529..83f96bca0 100644
--- a/charts/linkerd-control-plane/templates/destination-rbac.yaml
+++ b/charts/linkerd-control-plane/templates/destination-rbac.yaml
@@ -251,6 +251,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
diff --git a/charts/linkerd-crds/templates/policy/http-local-ratelimit-policy.yaml b/charts/linkerd-crds/templates/policy/http-local-ratelimit-policy.yaml
index 6ed6b6622..b53954945 100644
--- a/charts/linkerd-crds/templates/policy/http-local-ratelimit-policy.yaml
+++ b/charts/linkerd-crds/templates/policy/http-local-ratelimit-policy.yaml
@@ -125,6 +125,75 @@ spec:
                               maxLength: 253
                               minLength: 1
                               type: string
+            status:
+              type: object
+              properties:
+                conditions:
+                  type: array
+                  items:
+                    type: object
+                    properties:
+                      lastTransitionTime:
+                        description: lastTransitionTime is the last time the
+                          condition transitioned from one status to another.
+                        format: date-time
+                        type: string
+                      status:
+                        description: status of the condition (one of True, False, Unknown)
+                        enum:
+                        - "True"
+                        - "False"
+                        - Unknown
+                        type: string
+                      type:
+                        description: type of the condition in CamelCase or in
+                          foo.example.com/CamelCase.
+                        maxLength: 316
+                        pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                        type: string
+                      reason:
+                        description: reason contains a programmatic identifier
+                          indicating the reason for the condition's last
+                          transition. Producers of specific condition types may
+                          define expected values and meanings for this field, and
+                          whether the values are considered a guaranteed API. The
+                          value should be a CamelCase string. This field may not
+                          be empty.
+                        maxLength: 1024
+                        minLength: 1
+                        pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                        type: string
+                      message:
+                        description: message is a human readable message
+                          indicating details about the transition. This may be an
+                          empty string.
+                        maxLength: 32768
+                        type: string
+                  required:
+                  - status
+                  - type
+                targetRef:
+                  properties:
+                    group:
+                      default: policy.linkerd.io
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      default: Server
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    name:
+                      maxLength: 253
+                      minLength: 1
+                      type: string
+                  required:
+                  - name
+                  type: object
+              required:
+              - targetRef
       additionalPrinterColumns:
       - name: Target_kind
         description: The resource kind to which the rate-limit applies
diff --git a/policy-controller/k8s/api/src/policy.rs b/policy-controller/k8s/api/src/policy.rs
index 638909e0f..1a860f573 100644
--- a/policy-controller/k8s/api/src/policy.rs
+++ b/policy-controller/k8s/api/src/policy.rs
@@ -16,7 +16,10 @@ pub use self::{
     meshtls_authentication::{MeshTLSAuthentication, MeshTLSAuthenticationSpec},
     network::{Cidr, Network},
     network_authentication::{NetworkAuthentication, NetworkAuthenticationSpec},
-    ratelimit_policy::{HTTPLocalRateLimitPolicy, Limit, Override, RateLimitPolicySpec},
+    ratelimit_policy::{
+        HTTPLocalRateLimitPolicy, HTTPLocalRateLimitPolicyStatus, Limit, Override,
+        RateLimitPolicySpec,
+    },
     server::{Server, ServerSpec},
     server_authorization::{ServerAuthorization, ServerAuthorizationSpec},
     target_ref::{ClusterTargetRef, LocalTargetRef, NamespacedTargetRef},
diff --git a/policy-controller/k8s/api/src/policy/ratelimit_policy.rs b/policy-controller/k8s/api/src/policy/ratelimit_policy.rs
index ada219d13..6ddaab22c 100644
--- a/policy-controller/k8s/api/src/policy/ratelimit_policy.rs
+++ b/policy-controller/k8s/api/src/policy/ratelimit_policy.rs
@@ -1,4 +1,5 @@
 use super::{LocalTargetRef, NamespacedTargetRef};
+use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition;
 
 #[derive(
     Clone, Debug, kube::CustomResource, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
@@ -7,6 +8,7 @@ use super::{LocalTargetRef, NamespacedTargetRef};
     group = "policy.linkerd.io",
     version = "v1alpha1",
     kind = "HTTPLocalRateLimitPolicy",
+    status = "HTTPLocalRateLimitPolicyStatus",
     namespaced
 )]
 #[serde(rename_all = "camelCase")]
@@ -17,6 +19,13 @@ pub struct RateLimitPolicySpec {
     pub overrides: Option<Vec<Override>>,
 }
 
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct HTTPLocalRateLimitPolicyStatus {
+    pub conditions: Vec<Condition>,
+    pub target_ref: LocalTargetRef,
+}
+
 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct Limit {
diff --git a/policy-controller/k8s/index/src/inbound/index.rs b/policy-controller/k8s/index/src/inbound/index.rs
index c3aa7c4b6..178d9b2ff 100644
--- a/policy-controller/k8s/index/src/inbound/index.rs
+++ b/policy-controller/k8s/index/src/inbound/index.rs
@@ -1871,26 +1871,10 @@ impl PolicyIndex {
     fn client_ratelimit(&self, server_name: &str) -> Option<RateLimit> {
         use ratelimit_policy::{ClientRef, Target};
 
-        // sort the ratelimit policies by creation timestamp and name so we can
-        // deterministically always return the same policy when more than one point to the same
-        // server
-        let mut rate_limits = self
+        let (name, spec) = self
             .ratelimit_policies
             .iter()
-            .filter(|(_, spec)| matches!(spec.target, Target::Server(ref n) if n == server_name))
-            .collect::<Vec<_>>();
-        rate_limits.sort_by(|(a_name, a), (b_name, b)| {
-            let by_ts = match (&a.creation_timestamp, &b.creation_timestamp) {
-                (Some(a_ts), Some(b_ts)) => a_ts.cmp(b_ts),
-                (None, None) => std::cmp::Ordering::Equal,
-                // entries with timestamps are preferred over ones without
-                (Some(_), None) => return std::cmp::Ordering::Less,
-                (None, Some(_)) => return std::cmp::Ordering::Greater,
-            };
-            by_ts.then_with(|| a_name.cmp(b_name))
-        });
-
-        let (name, spec) = rate_limits.first()?;
+            .find(|(_, spec)| matches!(spec.target, Target::Server(ref n) if n == server_name && spec.accepted_by_server(server_name)))?;
 
         tracing::trace!(
             ns = %self.namespace,
diff --git a/policy-controller/k8s/index/src/inbound/ratelimit_policy.rs b/policy-controller/k8s/index/src/inbound/ratelimit_policy.rs
index d097ce111..f4fb99be8 100644
--- a/policy-controller/k8s/index/src/inbound/ratelimit_policy.rs
+++ b/policy-controller/k8s/index/src/inbound/ratelimit_policy.rs
@@ -5,6 +5,7 @@ use linkerd_policy_controller_k8s_api::{
     policy::{LocalTargetRef, NamespacedTargetRef},
     ServiceAccount, Time,
 };
+use std::fmt;
 
 #[derive(Debug, PartialEq)]
 pub(crate) struct Spec {
@@ -13,9 +14,10 @@ pub(crate) struct Spec {
     pub total: Option<Limit>,
     pub identity: Option<Limit>,
     pub overrides: Vec<Override>,
+    pub status: Status,
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Clone, Debug, Eq, PartialEq)]
 pub(crate) enum Target {
     Server(String),
 }
@@ -39,14 +41,67 @@ pub(crate) enum ClientRef {
     },
 }
 
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Status {
+    pub conditions: Vec<Condition>,
+    pub target: Target,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Condition {
+    pub type_: ConditionType,
+    pub status: bool,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum ConditionType {
+    Accepted,
+}
+
+impl Spec {
+    pub fn accepted_by_server(&self, name: &str) -> bool {
+        self.status.target == Target::Server(name.to_string())
+            && self
+                .status
+                .conditions
+                .iter()
+                .any(|condition| condition.type_ == ConditionType::Accepted && condition.status)
+    }
+}
+
 impl TryFrom<k8s::policy::HTTPLocalRateLimitPolicy> for Spec {
     type Error = anyhow::Error;
 
     fn try_from(rl: k8s::policy::HTTPLocalRateLimitPolicy) -> Result<Self> {
         let creation_timestamp = rl.metadata.creation_timestamp.map(|Time(t)| t);
+        let conditions = rl.status.map_or(vec![], |status| {
+            status
+            .conditions
+            .iter()
+            .filter_map(|condition| {
+                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");
+                        return None;
+                    }
+                };
+                let status = match condition.status.as_ref() {
+                    "True" => true,
+                    "False" => false,
+                    condition_status => {
+                        tracing::error!(%status.target_ref.name, %type_, %condition_status, "Unexpected condition status found in status");
+                        return None
+                    },
+                };
+                Some(Condition { type_, status })
+            }).collect()
+        });
+        let target = target(rl.spec.target_ref)?;
+
         Ok(Self {
             creation_timestamp,
-            target: target(rl.spec.target_ref)?,
+            target: target.clone(),
             total: rl.spec.total.map(|lim| Limit {
                 requests_per_second: lim.requests_per_second,
             }),
@@ -70,10 +125,19 @@ impl TryFrom<k8s::policy::HTTPLocalRateLimitPolicy> for Spec {
                     })
                 })
                 .collect::<Result<Vec<_>>>()?,
+            status: Status { conditions, target },
         })
     }
 }
 
+impl fmt::Display for ConditionType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Accepted => write!(f, "Accepted"),
+        }
+    }
+}
+
 fn target(t: LocalTargetRef) -> Result<Target> {
     match t {
         t if t.targets_kind::<k8s::policy::Server>() => Ok(Target::Server(t.name)),
diff --git a/policy-controller/k8s/status/src/index.rs b/policy-controller/k8s/status/src/index.rs
index b26f34fbe..4a5e2f4df 100644
--- a/policy-controller/k8s/status/src/index.rs
+++ b/policy-controller/k8s/status/src/index.rs
@@ -1,4 +1,5 @@
 use crate::{
+    ratelimit,
     resource_id::{NamespaceGroupKindName, ResourceId},
     routes,
     service::Service,
@@ -11,7 +12,7 @@ use linkerd_policy_controller_core::{routes::GroupKindName, IpNet, POLICY_CONTRO
 use linkerd_policy_controller_k8s_api::{
     self as k8s_core_api, gateway as k8s_gateway_api,
     policy::{self as linkerd_k8s_api, Cidr, Network},
-    NamespaceResourceScope, Resource, ResourceExt,
+    NamespaceResourceScope, Resource, ResourceExt, Time,
 };
 use parking_lot::RwLock;
 use prometheus_client::{
@@ -39,7 +40,9 @@ mod reasons {
     pub const BACKEND_NOT_FOUND: &str = "BackendNotFound";
     pub const INVALID_KIND: &str = "InvalidKind";
     pub const NO_MATCHING_PARENT: &str = "NoMatchingParent";
+    pub const NO_MATCHING_TARGET: &str = "NoMatchingTarget";
     pub const ROUTE_REASON_CONFLICTED: &str = "RouteReasonConflicted";
+    pub const RATELIMIT_REASON_ALREADY_EXISTS: &str = "RateLimitReasonAlreadyExists";
     pub const EGRESS_NET_REASON_OVERLAP: &str = "EgressReasonNetworkOverlap";
 }
 
@@ -86,6 +89,9 @@ pub struct Index {
     /// regardless of if those parents have accepted the route.
     route_refs: HashMap<NamespaceGroupKindName, RouteRef>,
 
+    /// Maps rate limit ids to a list of details about these rate limits.
+    ratelimits: HashMap<ResourceId, HTTPLocalRateLimitPolicyRef>,
+
     /// Maps egress network ids to a list of details about these networks.
     egress_networks: HashMap<ResourceId, EgressNetworkRef>,
 
@@ -108,6 +114,13 @@ struct RouteRef {
     statuses: Vec<k8s_gateway_api::RouteParentStatus>,
 }
 
+#[derive(Clone, PartialEq)]
+struct HTTPLocalRateLimitPolicyRef {
+    creation_timestamp: Option<DateTime<Utc>>,
+    target_ref: ratelimit::TargetReference,
+    status_conditions: Vec<k8s_core_api::Condition>,
+}
+
 #[derive(Clone, PartialEq)]
 struct EgressNetworkRef {
     networks: Vec<Network>,
@@ -266,6 +279,8 @@ impl Controller {
                             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;
                         }
@@ -332,6 +347,7 @@ impl Index {
             claims,
             updates,
             route_refs: HashMap::new(),
+            ratelimits: HashMap::new(),
             egress_networks: HashMap::new(),
             servers: HashSet::new(),
             services: HashMap::new(),
@@ -409,6 +425,27 @@ impl Index {
         true
     }
 
+    // If the rate limit is new or its contents have changed, return true, so that a patch is
+    // generated; otherwise return false.
+    fn update_ratelimit(
+        &mut self,
+        id: ResourceId,
+        ratelimit: &HTTPLocalRateLimitPolicyRef,
+    ) -> bool {
+        match self.ratelimits.entry(id) {
+            Entry::Vacant(entry) => {
+                entry.insert(ratelimit.clone());
+            }
+            Entry::Occupied(mut entry) => {
+                if entry.get() == ratelimit {
+                    return false;
+                }
+                entry.insert(ratelimit.clone());
+            }
+        }
+        true
+    }
+
     fn parent_status(
         &self,
         id: &NamespaceGroupKindName,
@@ -631,6 +668,77 @@ impl Index {
         }
     }
 
+    fn target_ref_status(
+        &self,
+        id: &NamespaceGroupKindName,
+        target_ref: &ratelimit::TargetReference,
+    ) -> Option<linkerd_k8s_api::HTTPLocalRateLimitPolicyStatus> {
+        match target_ref {
+            ratelimit::TargetReference::Server(server) => {
+                let condition = if self.servers.contains(server) {
+                    // Collect rate limits for this server, sorted by creation timestamp and then
+                    // by name. If the current RL is the first one in the list, it is accepted.
+                    let mut rate_limits = self
+                        .ratelimits
+                        .iter()
+                        .filter(|(_, rl_ref)| rl_ref.target_ref == *target_ref)
+                        .collect::<Vec<_>>();
+                    rate_limits.sort_by(|(a_id, a), (b_id, b)| {
+                        let by_ts = match (&a.creation_timestamp, &b.creation_timestamp) {
+                            (Some(a_ts), Some(b_ts)) => a_ts.cmp(b_ts),
+                            (None, None) => std::cmp::Ordering::Equal,
+                            // entries with timestamps are preferred over ones without
+                            (Some(_), None) => return std::cmp::Ordering::Less,
+                            (None, Some(_)) => return std::cmp::Ordering::Greater,
+                        };
+                        by_ts.then_with(|| a_id.name.cmp(&b_id.name))
+                    });
+
+                    let Some((first_id, _)) = rate_limits.first() else {
+                        // No rate limits exist for this server; we shouldn't reach this point!
+                        return None;
+                    };
+
+                    if first_id.name == id.gkn.name {
+                        accepted()
+                    } else {
+                        ratelimit_already_exists()
+                    }
+                } else {
+                    no_matching_target()
+                };
+
+                Some(linkerd_k8s_api::HTTPLocalRateLimitPolicyStatus {
+                    conditions: vec![condition],
+                    target_ref: linkerd_k8s_api::LocalTargetRef {
+                        group: Some(POLICY_API_GROUP.to_string()),
+                        kind: "Server".to_string(),
+                        name: server.name.clone(),
+                    },
+                })
+            }
+            ratelimit::TargetReference::UnknownKind => None,
+        }
+    }
+
+    fn make_ratelimit_patch(
+        &self,
+        id: &NamespaceGroupKindName,
+        ratelimit: &HTTPLocalRateLimitPolicyRef,
+    ) -> Option<k8s_core_api::Patch<serde_json::Value>> {
+        let status = self.target_ref_status(id, &ratelimit.target_ref);
+
+        let Some(status) = status else {
+            return None;
+        };
+
+        if eq_time_insensitive_conditions(&status.conditions, &ratelimit.status_conditions) {
+            return None;
+        }
+
+        make_patch(id, status)
+    }
+
     fn network_condition(&self, egress_net: &EgressNetworkRef) -> k8s_core_api::Condition {
         for egress_network_block in &egress_net.networks {
             for cluster_network_block in &self.cluster_networks {
@@ -714,6 +822,33 @@ impl Index {
                 }
             }
         }
+
+        // then update all ratelimit policies and their statuses
+        for (id, rl) in self.ratelimits.iter() {
+            let id = NamespaceGroupKindName {
+                namespace: id.namespace.clone(),
+                gkn: GroupKindName {
+                    group: linkerd_k8s_api::HTTPLocalRateLimitPolicy::group(&()),
+                    kind: linkerd_k8s_api::HTTPLocalRateLimitPolicy::kind(&()),
+                    name: id.name.clone().into(),
+                },
+            };
+
+            if let Some(patch) = self.make_ratelimit_patch(&id, rl) {
+                match self.updates.try_send(Update {
+                    id: id.clone(),
+                    patch,
+                }) {
+                    Ok(()) => {
+                        self.metrics.patch_enqueues.inc();
+                    }
+                    Err(error) => {
+                        self.metrics.patch_channel_full.inc();
+                        tracing::error!(%id.namespace, ratelimit = ?id.gkn, %error, "Failed to send ratelimit patch");
+                    }
+                }
+            }
+        }
     }
 }
 
@@ -1085,6 +1220,45 @@ impl kubert::index::IndexNamespacedResource<k8s_core_api::Service> for Index {
     // to handle resets specially.
 }
 
+impl kubert::index::IndexNamespacedResource<linkerd_k8s_api::HTTPLocalRateLimitPolicy> for Index {
+    fn apply(&mut self, resource: linkerd_k8s_api::HTTPLocalRateLimitPolicy) {
+        let namespace = resource
+            .namespace()
+            .expect("HTTPLocalRateLimitPolicy must have a namespace");
+        let name = resource.name_unchecked();
+
+        let status_conditions = resource
+            .status
+            .into_iter()
+            .flat_map(|s| s.conditions)
+            .collect();
+
+        let id = ResourceId::new(namespace.clone(), name);
+        let creation_timestamp = resource.metadata.creation_timestamp.map(|Time(t)| t);
+        let target_ref = ratelimit::TargetReference::make_target_ref(&namespace, &resource.spec);
+
+        let rl = HTTPLocalRateLimitPolicyRef {
+            creation_timestamp,
+            target_ref,
+            status_conditions,
+        };
+
+        self.index_ratelimit(id, rl);
+    }
+
+    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();
+    }
+}
+
 impl kubert::index::IndexNamespacedResource<linkerd_k8s_api::EgressNetwork> for Index {
     fn apply(&mut self, resource: linkerd_k8s_api::EgressNetwork) {
         let namespace = resource
@@ -1174,6 +1348,23 @@ impl Index {
 
         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()
+    }
 }
 
 pub(crate) fn make_patch<Status>(
@@ -1220,6 +1411,17 @@ pub(crate) fn no_matching_parent() -> k8s_core_api::Condition {
     }
 }
 
+pub(crate) fn no_matching_target() -> k8s_core_api::Condition {
+    k8s_core_api::Condition {
+        last_transition_time: k8s_core_api::Time(now()),
+        message: "".to_string(),
+        observed_generation: None,
+        reason: reasons::NO_MATCHING_TARGET.to_string(),
+        status: cond_statuses::STATUS_FALSE.to_string(),
+        type_: conditions::ACCEPTED.to_string(),
+    }
+}
+
 fn headless_parent() -> k8s_core_api::Condition {
     k8s_core_api::Condition {
         last_transition_time: k8s_core_api::Time(now()),
@@ -1253,6 +1455,17 @@ pub(crate) fn route_conflicted() -> k8s_core_api::Condition {
     }
 }
 
+pub(crate) fn ratelimit_already_exists() -> k8s_core_api::Condition {
+    k8s_core_api::Condition {
+        last_transition_time: k8s_core_api::Time(now()),
+        message: "".to_string(),
+        observed_generation: None,
+        reason: reasons::RATELIMIT_REASON_ALREADY_EXISTS.to_string(),
+        status: cond_statuses::STATUS_FALSE.to_string(),
+        type_: conditions::ACCEPTED.to_string(),
+    }
+}
+
 pub(crate) fn accepted() -> k8s_core_api::Condition {
     k8s_core_api::Condition {
         last_transition_time: k8s_core_api::Time(now()),
diff --git a/policy-controller/k8s/status/src/lib.rs b/policy-controller/k8s/status/src/lib.rs
index 5dd710c34..f4d3428a0 100644
--- a/policy-controller/k8s/status/src/lib.rs
+++ b/policy-controller/k8s/status/src/lib.rs
@@ -1,4 +1,5 @@
 mod index;
+mod ratelimit;
 mod resource_id;
 mod routes;
 mod service;
diff --git a/policy-controller/k8s/status/src/ratelimit.rs b/policy-controller/k8s/status/src/ratelimit.rs
new file mode 100644
index 000000000..f8e8bce83
--- /dev/null
+++ b/policy-controller/k8s/status/src/ratelimit.rs
@@ -0,0 +1,24 @@
+use crate::resource_id::ResourceId;
+use linkerd_policy_controller_k8s_api::policy as linkerd_k8s_api;
+
+#[derive(Clone, Eq, PartialEq)]
+pub enum TargetReference {
+    Server(ResourceId),
+    UnknownKind,
+}
+
+impl TargetReference {
+    pub(crate) fn make_target_ref(
+        namespace: &str,
+        rl: &linkerd_k8s_api::RateLimitPolicySpec,
+    ) -> TargetReference {
+        if rl.target_ref.targets_kind::<linkerd_k8s_api::Server>() {
+            Self::Server(ResourceId::new(
+                namespace.to_string(),
+                rl.target_ref.name.clone(),
+            ))
+        } else {
+            Self::UnknownKind
+        }
+    }
+}
diff --git a/policy-controller/k8s/status/src/resource_id.rs b/policy-controller/k8s/status/src/resource_id.rs
index 557b501ae..4ec4a2d88 100644
--- a/policy-controller/k8s/status/src/resource_id.rs
+++ b/policy-controller/k8s/status/src/resource_id.rs
@@ -27,6 +27,9 @@ impl NamespaceGroupKindName {
     pub fn api_version(&self) -> anyhow::Result<Cow<'static, str>> {
         match (self.gkn.group.as_ref(), self.gkn.kind.as_ref()) {
             (POLICY_API_GROUP, "HTTPRoute") => Ok(linkerd_k8s_api::HttpRoute::api_version(&())),
+            (POLICY_API_GROUP, "HTTPLocalRateLimitPolicy") => {
+                Ok(linkerd_k8s_api::HTTPLocalRateLimitPolicy::api_version(&()))
+            }
             (POLICY_API_GROUP, "EgressNetwork") => {
                 Ok(linkerd_k8s_api::EgressNetwork::api_version(&()))
             }
diff --git a/policy-controller/src/main.rs b/policy-controller/src/main.rs
index ba6393043..af6a898dc 100644
--- a/policy-controller/src/main.rs
+++ b/policy-controller/src/main.rs
@@ -255,8 +255,11 @@ async fn main() -> Result<()> {
 
     let ratelimit_policies =
         runtime.watch_all::<k8s::policy::HTTPLocalRateLimitPolicy>(watcher::Config::default());
+    let ratelimit_policies_indexes = IndexList::new(inbound_index.clone())
+        .push(status_index.clone())
+        .shared();
     tokio::spawn(
-        kubert::index::namespaced(inbound_index.clone(), ratelimit_policies)
+        kubert::index::namespaced(ratelimit_policies_indexes.clone(), ratelimit_policies)
             .instrument(info_span!("httplocalratelimitpolicies")),
     );
 


Test patch:
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 12c817837..e015bbd81 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1346,7 +1347,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_crds.golden b/cli/cmd/testdata/install_crds.golden
index 1b9858fec..8fe4aca7d 100644
--- a/cli/cmd/testdata/install_crds.golden
+++ b/cli/cmd/testdata/install_crds.golden
@@ -347,6 +347,75 @@ spec:
                               maxLength: 253
                               minLength: 1
                               type: string
+            status:
+              type: object
+              properties:
+                conditions:
+                  type: array
+                  items:
+                    type: object
+                    properties:
+                      lastTransitionTime:
+                        description: lastTransitionTime is the last time the
+                          condition transitioned from one status to another.
+                        format: date-time
+                        type: string
+                      status:
+                        description: status of the condition (one of True, False, Unknown)
+                        enum:
+                        - "True"
+                        - "False"
+                        - Unknown
+                        type: string
+                      type:
+                        description: type of the condition in CamelCase or in
+                          foo.example.com/CamelCase.
+                        maxLength: 316
+                        pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                        type: string
+                      reason:
+                        description: reason contains a programmatic identifier
+                          indicating the reason for the condition's last
+                          transition. Producers of specific condition types may
+                          define expected values and meanings for this field, and
+                          whether the values are considered a guaranteed API. The
+                          value should be a CamelCase string. This field may not
+                          be empty.
+                        maxLength: 1024
+                        minLength: 1
+                        pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                        type: string
+                      message:
+                        description: message is a human readable message
+                          indicating details about the transition. This may be an
+                          empty string.
+                        maxLength: 32768
+                        type: string
+                  required:
+                  - status
+                  - type
+                targetRef:
+                  properties:
+                    group:
+                      default: policy.linkerd.io
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      default: Server
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    name:
+                      maxLength: 253
+                      minLength: 1
+                      type: string
+                  required:
+                  - name
+                  type: object
+              required:
+              - targetRef
       additionalPrinterColumns:
       - name: Target_kind
         description: The resource kind to which the rate-limit applies
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index 468d32565..d66256fc6 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1345,7 +1346,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index e658b7cda..dbf0f151e 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1345,7 +1346,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 468d32565..d66256fc6 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1345,7 +1346,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
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 e7a847e5e..d719d58d1 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1345,7 +1346,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden
index e1e24fa88..54193141d 100644
--- a/cli/cmd/testdata/install_default_token.golden
+++ b/cli/cmd/testdata/install_default_token.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1336,7 +1337,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_gid_output.golden b/cli/cmd/testdata/install_gid_output.golden
index 54a46d2fb..5b16cc2d3 100755
--- a/cli/cmd/testdata/install_gid_output.golden
+++ b/cli/cmd/testdata/install_gid_output.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1349,7 +1350,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index d7df17756..9d31f777a 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1448,7 +1449,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: b53320781231a300833e58dd60e59dfc76812535b6d8baceac8e9aebf532115f
+        checksum/config: 33fbe1b09d768bf7ac30457535bac171e47cd5f078b7bda7c15f8e1de2943659
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 618fd20c8..d713fd2f1 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1448,7 +1449,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: b53320781231a300833e58dd60e59dfc76812535b6d8baceac8e9aebf532115f
+        checksum/config: 33fbe1b09d768bf7ac30457535bac171e47cd5f078b7bda7c15f8e1de2943659
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 18353d6d0..75bbe4d5c 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1276,7 +1277,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_helm_control_plane_output.golden b/cli/cmd/testdata/install_helm_control_plane_output.golden
index ead77ed5e..a59741c66 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output.golden
@@ -237,6 +237,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1320,7 +1321,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 135a435d8eac25e6b0cb52bdcb59c99c293707c5ead497c969ab6a0fdbbd2017
+        checksum/config: 024027c6115d84206f869e9becc360639cf77cf24b0e9c6c8920138f7ea33fd2
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
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 4bdde775c..ea2d0fef0 100644
--- a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden
@@ -237,6 +237,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1423,7 +1424,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 1c02562d84ed479248921e406146be0e36935c87b0d28a1c903c3b90a117cc5b
+        checksum/config: fd8add3a34efa33eae90f8e70a5e0925728e8cf48d5607c4c03adb27ed005fe1
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
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 5d6596c87..89fd70713 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
@@ -237,6 +237,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1427,7 +1428,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 1c02562d84ed479248921e406146be0e36935c87b0d28a1c903c3b90a117cc5b
+        checksum/config: fd8add3a34efa33eae90f8e70a5e0925728e8cf48d5607c4c03adb27ed005fe1
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_helm_crds_output.golden b/cli/cmd/testdata/install_helm_crds_output.golden
index 1227d2469..0a08417d0 100644
--- a/cli/cmd/testdata/install_helm_crds_output.golden
+++ b/cli/cmd/testdata/install_helm_crds_output.golden
@@ -353,6 +353,75 @@ spec:
                               maxLength: 253
                               minLength: 1
                               type: string
+            status:
+              type: object
+              properties:
+                conditions:
+                  type: array
+                  items:
+                    type: object
+                    properties:
+                      lastTransitionTime:
+                        description: lastTransitionTime is the last time the
+                          condition transitioned from one status to another.
+                        format: date-time
+                        type: string
+                      status:
+                        description: status of the condition (one of True, False, Unknown)
+                        enum:
+                        - "True"
+                        - "False"
+                        - Unknown
+                        type: string
+                      type:
+                        description: type of the condition in CamelCase or in
+                          foo.example.com/CamelCase.
+                        maxLength: 316
+                        pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                        type: string
+                      reason:
+                        description: reason contains a programmatic identifier
+                          indicating the reason for the condition's last
+                          transition. Producers of specific condition types may
+                          define expected values and meanings for this field, and
+                          whether the values are considered a guaranteed API. The
+                          value should be a CamelCase string. This field may not
+                          be empty.
+                        maxLength: 1024
+                        minLength: 1
+                        pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                        type: string
+                      message:
+                        description: message is a human readable message
+                          indicating details about the transition. This may be an
+                          empty string.
+                        maxLength: 32768
+                        type: string
+                  required:
+                  - status
+                  - type
+                targetRef:
+                  properties:
+                    group:
+                      default: policy.linkerd.io
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      default: Server
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    name:
+                      maxLength: 253
+                      minLength: 1
+                      type: string
+                  required:
+                  - name
+                  type: object
+              required:
+              - targetRef
       additionalPrinterColumns:
       - name: Target_kind
         description: The resource kind to which the rate-limit applies
diff --git a/cli/cmd/testdata/install_helm_crds_output_ha.golden b/cli/cmd/testdata/install_helm_crds_output_ha.golden
index 1227d2469..0a08417d0 100644
--- a/cli/cmd/testdata/install_helm_crds_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_crds_output_ha.golden
@@ -353,6 +353,75 @@ spec:
                               maxLength: 253
                               minLength: 1
                               type: string
+            status:
+              type: object
+              properties:
+                conditions:
+                  type: array
+                  items:
+                    type: object
+                    properties:
+                      lastTransitionTime:
+                        description: lastTransitionTime is the last time the
+                          condition transitioned from one status to another.
+                        format: date-time
+                        type: string
+                      status:
+                        description: status of the condition (one of True, False, Unknown)
+                        enum:
+                        - "True"
+                        - "False"
+                        - Unknown
+                        type: string
+                      type:
+                        description: type of the condition in CamelCase or in
+                          foo.example.com/CamelCase.
+                        maxLength: 316
+                        pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                        type: string
+                      reason:
+                        description: reason contains a programmatic identifier
+                          indicating the reason for the condition's last
+                          transition. Producers of specific condition types may
+                          define expected values and meanings for this field, and
+                          whether the values are considered a guaranteed API. The
+                          value should be a CamelCase string. This field may not
+                          be empty.
+                        maxLength: 1024
+                        minLength: 1
+                        pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                        type: string
+                      message:
+                        description: message is a human readable message
+                          indicating details about the transition. This may be an
+                          empty string.
+                        maxLength: 32768
+                        type: string
+                  required:
+                  - status
+                  - type
+                targetRef:
+                  properties:
+                    group:
+                      default: policy.linkerd.io
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      default: Server
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    name:
+                      maxLength: 253
+                      minLength: 1
+                      type: string
+                  required:
+                  - name
+                  type: object
+              required:
+              - targetRef
       additionalPrinterColumns:
       - name: Target_kind
         description: The resource kind to which the rate-limit applies
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index 41417f8e4..fd147498b 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -237,6 +237,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1431,7 +1432,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 1c02562d84ed479248921e406146be0e36935c87b0d28a1c903c3b90a117cc5b
+        checksum/config: fd8add3a34efa33eae90f8e70a5e0925728e8cf48d5607c4c03adb27ed005fe1
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
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 1f4d5df31..ed30e7972 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -237,6 +237,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1413,7 +1414,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 412d4349fe71497326ed1946ce0d9c31985fd1db5c8d6a1be1d681987d0c6a33
+        checksum/config: 882b51dcfef64f0e2418f2865db47c80b988a2dc0132c3e927a0b92ce3df8ad1
         linkerd.io/created-by: linkerd/helm linkerd-version
         linkerd.io/proxy-version: test-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 21f8a3005..7fe206bb5 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1338,7 +1339,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 237438037..f16ca02bd 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -243,6 +243,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1284,7 +1285,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 6b107c2b9d26d1c49f0d8912d5793a0c52438cdd5b3de537d13de0ec18da365e
+        checksum/config: 036fb1916c0896bb3c88168f51e2fc8846b00678b206ea8a14361ad4df1ae1bc
         linkerd.io/created-by: CliVersion
         linkerd.io/proxy-version: ProxyVersion
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index cc33497a6..cf7248609 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1345,7 +1346,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index 8245269b6..aa2647248 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -246,6 +246,7 @@ rules:
       - policy.linkerd.io
     resources:
       - httproutes/status
+      - httplocalratelimitpolicies/status
       - egressnetworks/status
     verbs:
       - patch
@@ -1345,7 +1346,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 726a0d9a61f222ca9efb64bc6ec38ad1d4beeab125c79f00b45a1d64be642875
+        checksum/config: a39770e2a1ac1f4bb452509e52a4ffd176bcb034f1ce906491f2649e28c67319
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/proxy-version: install-proxy-version
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
diff --git a/policy-controller/k8s/index/src/inbound/tests/ratelimit_policy.rs b/policy-controller/k8s/index/src/inbound/tests/ratelimit_policy.rs
index ab06f98b1..9c576f082 100644
--- a/policy-controller/k8s/index/src/inbound/tests/ratelimit_policy.rs
+++ b/policy-controller/k8s/index/src/inbound/tests/ratelimit_policy.rs
@@ -105,5 +105,20 @@ fn mk_ratelimit(
             identity: None,
             overrides: Some(overrides),
         },
+        status: Some(k8s::policy::HTTPLocalRateLimitPolicyStatus {
+            conditions: vec![k8s::Condition {
+                last_transition_time: k8s::Time(chrono::DateTime::<chrono::Utc>::MIN_UTC),
+                message: "".to_string(),
+                observed_generation: None,
+                reason: "".to_string(),
+                status: "True".to_string(),
+                type_: "Accepted".to_string(),
+            }],
+            target_ref: LocalTargetRef {
+                group: Some("policy.linkerd.io".to_string()),
+                kind: "Server".to_string(),
+                name: server_name.to_string(),
+            },
+        }),
     }
 }
diff --git a/policy-controller/k8s/status/src/tests.rs b/policy-controller/k8s/status/src/tests.rs
index 6d7b0e5db..5aa196b6f 100644
--- a/policy-controller/k8s/status/src/tests.rs
+++ b/policy-controller/k8s/status/src/tests.rs
@@ -1,5 +1,7 @@
 use linkerd_policy_controller_core::IpNet;
+use linkerd_policy_controller_k8s_api::{self as k8s_core_api, policy as linkerd_k8s_api};
 mod egress_network;
+mod ratelimit;
 mod routes;
 
 pub fn default_cluster_networks() -> Vec<IpNet> {
@@ -11,3 +13,33 @@ pub fn default_cluster_networks() -> Vec<IpNet> {
         "fd00::/8".parse().unwrap(),
     ]
 }
+
+pub fn make_server(
+    namespace: impl ToString,
+    name: impl ToString,
+    port: u16,
+    srv_labels: impl IntoIterator<Item = (&'static str, &'static str)>,
+    pod_labels: impl IntoIterator<Item = (&'static str, &'static str)>,
+    proxy_protocol: Option<linkerd_k8s_api::server::ProxyProtocol>,
+) -> linkerd_k8s_api::Server {
+    let port = linkerd_k8s_api::server::Port::Number(port.try_into().unwrap());
+    linkerd_k8s_api::Server {
+        metadata: k8s_core_api::ObjectMeta {
+            namespace: Some(namespace.to_string()),
+            name: Some(name.to_string()),
+            labels: Some(
+                srv_labels
+                    .into_iter()
+                    .map(|(k, v)| (k.to_string(), v.to_string()))
+                    .collect(),
+            ),
+            ..Default::default()
+        },
+        spec: linkerd_k8s_api::ServerSpec {
+            port,
+            selector: linkerd_k8s_api::server::Selector::Pod(pod_labels.into_iter().collect()),
+            proxy_protocol,
+            access_policy: None,
+        },
+    }
+}
diff --git a/policy-controller/k8s/status/src/tests/ratelimit.rs b/policy-controller/k8s/status/src/tests/ratelimit.rs
new file mode 100644
index 000000000..baaae731e
--- /dev/null
+++ b/policy-controller/k8s/status/src/tests/ratelimit.rs
@@ -0,0 +1,218 @@
+use crate::{
+    index::{accepted, no_matching_target, ratelimit_already_exists, SharedIndex, Update},
+    resource_id::NamespaceGroupKindName,
+    tests::{default_cluster_networks, make_server},
+    Index, IndexMetrics,
+};
+use chrono::{DateTime, Utc};
+use kubert::index::IndexNamespacedResource;
+use linkerd_policy_controller_core::routes::GroupKindName;
+use linkerd_policy_controller_k8s_api::{
+    self as k8s_core_api,
+    policy::{self as linkerd_k8s_api},
+    Resource,
+};
+use std::sync::Arc;
+use tokio::sync::{
+    mpsc::{self, Receiver},
+    watch,
+};
+
+#[test]
+fn ratelimit_accepted() {
+    let (index, mut updates_rx) = make_index_updates_rx();
+
+    // create server
+    let server = make_server(
+        "ns",
+        "server-1",
+        8080,
+        vec![("app", "server")],
+        vec![],
+        None,
+    );
+    index.write().apply(server);
+
+    // create an associated rate limit
+    let (ratelimit_id, ratelimit) = make_ratelimit("rl-1".to_string(), "server-1".to_string());
+    index.write().apply(ratelimit);
+
+    let expected_status = linkerd_k8s_api::HTTPLocalRateLimitPolicyStatus {
+        conditions: vec![accepted()],
+        target_ref: linkerd_k8s_api::LocalTargetRef {
+            group: Some("policy.linkerd.io".to_string()),
+            kind: "Server".to_string(),
+            name: "server-1".to_string(),
+        },
+    };
+
+    let expected_patch = crate::index::make_patch(&ratelimit_id, expected_status).unwrap();
+
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(ratelimit_id, update.id);
+    assert_eq!(expected_patch, update.patch);
+    assert!(updates_rx.try_recv().is_err())
+}
+
+#[test]
+fn ratelimit_not_accepted_no_matching_target() {
+    let (index, mut updates_rx) = make_index_updates_rx();
+
+    // create server
+    let server = make_server(
+        "ns",
+        "server-1",
+        8080,
+        vec![("app", "server")],
+        vec![],
+        None,
+    );
+    index.write().apply(server);
+
+    // create an associated rate limit
+    let (ratelimit_id, ratelimit) = make_ratelimit("rl-1".to_string(), "server-2".to_string());
+    index.write().apply(ratelimit);
+
+    let expected_status = linkerd_k8s_api::HTTPLocalRateLimitPolicyStatus {
+        conditions: vec![no_matching_target()],
+        target_ref: linkerd_k8s_api::LocalTargetRef {
+            group: Some("policy.linkerd.io".to_string()),
+            kind: "Server".to_string(),
+            name: "server-2".to_string(),
+        },
+    };
+
+    let expected_patch = crate::index::make_patch(&ratelimit_id, expected_status).unwrap();
+
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(ratelimit_id, update.id);
+    assert_eq!(expected_patch, update.patch);
+    assert!(updates_rx.try_recv().is_err())
+}
+
+#[test]
+fn ratelimit_not_accepted_already_exists() {
+    let (index, mut updates_rx) = make_index_updates_rx();
+
+    // create server
+    let server = make_server(
+        "ns",
+        "server-1",
+        8080,
+        vec![("app", "server")],
+        vec![],
+        None,
+    );
+    index.write().apply(server);
+
+    // create an associated rate limit
+    let (rl_1_id, rl_1) = make_ratelimit("rl-1".to_string(), "server-1".to_string());
+    index.write().apply(rl_1);
+
+    let expected_status = linkerd_k8s_api::HTTPLocalRateLimitPolicyStatus {
+        conditions: vec![accepted()],
+        target_ref: linkerd_k8s_api::LocalTargetRef {
+            group: Some("policy.linkerd.io".to_string()),
+            kind: "Server".to_string(),
+            name: "server-1".to_string(),
+        },
+    };
+
+    let rl_1_expected_patch = crate::index::make_patch(&rl_1_id, expected_status).unwrap();
+
+    let update = updates_rx.try_recv().unwrap();
+    assert_eq!(rl_1_id, update.id);
+    assert_eq!(rl_1_expected_patch, update.patch);
+    assert!(updates_rx.try_recv().is_err());
+
+    // create another rate limit for the same server
+    let (rl_2_id, rl_2) = make_ratelimit("rl-2".to_string(), "server-1".to_string());
+    index.write().apply(rl_2);
+
+    let expected_status = linkerd_k8s_api::HTTPLocalRateLimitPolicyStatus {
+        conditions: vec![ratelimit_already_exists()],
+        target_ref: linkerd_k8s_api::LocalTargetRef {
+            group: Some("policy.linkerd.io".to_string()),
+            kind: "Server".to_string(),
+            name: "server-1".to_string(),
+        },
+    };
+
+    let rl_2_expected_patch = crate::index::make_patch(&rl_2_id, expected_status).unwrap();
+
+    let update_1 = updates_rx.try_recv().unwrap();
+    let update_2 = updates_rx.try_recv().unwrap();
+    assert!(updates_rx.try_recv().is_err());
+
+    // we should receive updates for both rate limits in any order
+    if update_1.id == rl_1_id {
+        assert_eq!(rl_1_id, update_1.id);
+        assert_eq!(rl_1_expected_patch, update_1.patch);
+        assert_eq!(rl_2_id, update_2.id);
+        assert_eq!(rl_2_expected_patch, update_2.patch);
+    } else {
+        assert_eq!(rl_1_id, update_2.id);
+        assert_eq!(rl_1_expected_patch, update_2.patch);
+        assert_eq!(rl_2_id, update_1.id);
+        assert_eq!(rl_2_expected_patch, update_1.patch);
+    }
+}
+
+fn make_index_updates_rx() -> (SharedIndex, Receiver<Update>) {
+    let hostname = "test";
+    let claim = kubert::lease::Claim {
+        holder: "test".to_string(),
+        expiry: DateTime::<Utc>::MAX_UTC,
+    };
+    let (_claims_tx, claims_rx) = watch::channel(Arc::new(claim));
+    let (updates_tx, updates_rx) = mpsc::channel(10000);
+    let index = Index::shared(
+        hostname,
+        claims_rx,
+        updates_tx,
+        IndexMetrics::register(&mut Default::default()),
+        default_cluster_networks(),
+    );
+
+    (index, updates_rx)
+}
+
+fn make_ratelimit(
+    name: String,
+    server: String,
+) -> (
+    NamespaceGroupKindName,
+    linkerd_k8s_api::HTTPLocalRateLimitPolicy,
+) {
+    let ratelimit_id = NamespaceGroupKindName {
+        namespace: "ns".to_string(),
+        gkn: GroupKindName {
+            group: linkerd_k8s_api::HTTPLocalRateLimitPolicy::group(&()),
+            kind: linkerd_k8s_api::HTTPLocalRateLimitPolicy::kind(&()),
+            name: name.clone().into(),
+        },
+    };
+
+    let ratelimit = linkerd_k8s_api::HTTPLocalRateLimitPolicy {
+        metadata: k8s_core_api::ObjectMeta {
+            name: Some(name),
+            namespace: Some("ns".to_string()),
+            ..Default::default()
+        },
+        spec: linkerd_k8s_api::RateLimitPolicySpec {
+            target_ref: linkerd_k8s_api::LocalTargetRef {
+                group: Some("policy.linkerd.io".to_string()),
+                kind: "Server".to_string(),
+                name: server,
+            },
+            total: Some(linkerd_k8s_api::Limit {
+                requests_per_second: 1,
+            }),
+            identity: None,
+            overrides: None,
+        },
+        status: None,
+    };
+
+    (ratelimit_id, ratelimit)
+}
diff --git a/policy-controller/k8s/status/src/tests/routes.rs b/policy-controller/k8s/status/src/tests/routes.rs
index 58f094372..b3063cd2f 100644
--- a/policy-controller/k8s/status/src/tests/routes.rs
+++ b/policy-controller/k8s/status/src/tests/routes.rs
@@ -88,33 +88,3 @@ fn make_egress_network(
         }),
     }
 }
-
-fn make_server(
-    namespace: impl ToString,
-    name: impl ToString,
-    port: u16,
-    srv_labels: impl IntoIterator<Item = (&'static str, &'static str)>,
-    pod_labels: impl IntoIterator<Item = (&'static str, &'static str)>,
-    proxy_protocol: Option<linkerd_k8s_api::server::ProxyProtocol>,
-) -> linkerd_k8s_api::Server {
-    let port = linkerd_k8s_api::server::Port::Number(port.try_into().unwrap());
-    linkerd_k8s_api::Server {
-        metadata: k8s_core_api::ObjectMeta {
-            namespace: Some(namespace.to_string()),
-            name: Some(name.to_string()),
-            labels: Some(
-                srv_labels
-                    .into_iter()
-                    .map(|(k, v)| (k.to_string(), v.to_string()))
-                    .collect(),
-            ),
-            ..Default::default()
-        },
-        spec: linkerd_k8s_api::ServerSpec {
-            port,
-            selector: linkerd_k8s_api::server::Selector::Pod(pod_labels.into_iter().collect()),
-            proxy_protocol,
-            access_policy: None,
-        },
-    }
-}
diff --git a/policy-controller/k8s/status/src/tests/routes/grpc.rs b/policy-controller/k8s/status/src/tests/routes/grpc.rs
index fab43ea42..4330562a9 100644
--- a/policy-controller/k8s/status/src/tests/routes/grpc.rs
+++ b/policy-controller/k8s/status/src/tests/routes/grpc.rs
@@ -5,7 +5,7 @@ use crate::{
         route_conflicted, POLICY_API_GROUP,
     },
     resource_id::NamespaceGroupKindName,
-    tests::default_cluster_networks,
+    tests::{default_cluster_networks, make_server},
     Index, IndexMetrics,
 };
 use chrono::{DateTime, Utc};
@@ -564,7 +564,7 @@ fn route_accepted_after_server_create() {
     assert_eq!(patch, update.patch);
 
     // Apply the server
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
@@ -685,7 +685,7 @@ fn route_rejected_after_server_delete() {
         default_cluster_networks(),
     );
 
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
diff --git a/policy-controller/k8s/status/src/tests/routes/http.rs b/policy-controller/k8s/status/src/tests/routes/http.rs
index d9f4d24b9..f9d41d93c 100644
--- a/policy-controller/k8s/status/src/tests/routes/http.rs
+++ b/policy-controller/k8s/status/src/tests/routes/http.rs
@@ -5,7 +5,7 @@ use crate::{
         POLICY_API_GROUP,
     },
     resource_id::NamespaceGroupKindName,
-    tests::default_cluster_networks,
+    tests::{default_cluster_networks, make_server},
     Index, IndexMetrics,
 };
 use chrono::{DateTime, Utc};
@@ -1209,7 +1209,7 @@ fn linkerd_route_accepted_after_server_create() {
     assert_eq!(patch, update.patch);
 
     // Apply the server
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
@@ -1370,7 +1370,7 @@ fn gateway_route_accepted_after_server_create() {
     assert_eq!(patch, update.patch);
 
     // Apply the server
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
@@ -1491,7 +1491,7 @@ fn linkerd_route_rejected_after_server_delete() {
         default_cluster_networks(),
     );
 
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
@@ -1669,7 +1669,7 @@ fn gateway_route_rejected_after_server_delete() {
         default_cluster_networks(),
     );
 
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
diff --git a/policy-controller/k8s/status/src/tests/routes/tcp.rs b/policy-controller/k8s/status/src/tests/routes/tcp.rs
index ba5a041c6..49c559590 100644
--- a/policy-controller/k8s/status/src/tests/routes/tcp.rs
+++ b/policy-controller/k8s/status/src/tests/routes/tcp.rs
@@ -5,7 +5,7 @@ use crate::{
         POLICY_API_GROUP,
     },
     resource_id::NamespaceGroupKindName,
-    tests::default_cluster_networks,
+    tests::{default_cluster_networks, make_server},
     Index, IndexMetrics,
 };
 use chrono::{DateTime, Utc};
@@ -556,7 +556,7 @@ fn route_accepted_after_server_create() {
     assert_eq!(patch, update.patch);
 
     // Apply the server
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
@@ -677,7 +677,7 @@ fn route_rejected_after_server_delete() {
         default_cluster_networks(),
     );
 
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
diff --git a/policy-controller/k8s/status/src/tests/routes/tls.rs b/policy-controller/k8s/status/src/tests/routes/tls.rs
index 5689d994b..d5d214808 100644
--- a/policy-controller/k8s/status/src/tests/routes/tls.rs
+++ b/policy-controller/k8s/status/src/tests/routes/tls.rs
@@ -5,7 +5,7 @@ use crate::{
         route_conflicted, POLICY_API_GROUP,
     },
     resource_id::NamespaceGroupKindName,
-    tests::default_cluster_networks,
+    tests::{default_cluster_networks, make_server},
     Index, IndexMetrics,
 };
 use chrono::{DateTime, Utc};
@@ -556,7 +556,7 @@ fn route_accepted_after_server_create() {
     assert_eq!(patch, update.patch);
 
     // Apply the server
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
@@ -677,7 +677,7 @@ fn route_rejected_after_server_delete() {
         default_cluster_networks(),
     );
 
-    let server = super::make_server(
+    let server = make_server(
         "ns-0",
         "srv-8080",
         8080,
diff --git a/policy-test/tests/admit_http_local_ratelimit_policy.rs b/policy-test/tests/admit_http_local_ratelimit_policy.rs
index 5e4de4994..71194242d 100644
--- a/policy-test/tests/admit_http_local_ratelimit_policy.rs
+++ b/policy-test/tests/admit_http_local_ratelimit_policy.rs
@@ -1,8 +1,9 @@
+use k8s_openapi::chrono;
 use linkerd_policy_controller_k8s_api::{
     self as api,
     policy::{
-        HTTPLocalRateLimitPolicy, Limit, LocalTargetRef, NamespacedTargetRef, Override,
-        RateLimitPolicySpec,
+        HTTPLocalRateLimitPolicy, HTTPLocalRateLimitPolicyStatus, Limit, LocalTargetRef,
+        NamespacedTargetRef, Override, RateLimitPolicySpec,
     },
 };
 use linkerd_policy_test::admission;
@@ -90,5 +91,20 @@ fn mk_ratelimiter(
             }),
             overrides: Some(overrides),
         },
+        status: Some(HTTPLocalRateLimitPolicyStatus {
+            conditions: vec![api::Condition {
+                last_transition_time: api::Time(chrono::DateTime::<chrono::Utc>::MIN_UTC),
+                message: "".to_string(),
+                observed_generation: None,
+                reason: "".to_string(),
+                status: "True".to_string(),
+                type_: "Accepted".to_string(),
+            }],
+            target_ref: LocalTargetRef {
+                group: Some("policy.linkerd.io".to_string()),
+                kind: "Server".to_string(),
+                name: "linkerd-admin".to_string(),
+            },
+        }),
     }
 }
diff --git a/policy-test/tests/inbound_api.rs b/policy-test/tests/inbound_api.rs
index 2efeb2c3b..b17880c0e 100644
--- a/policy-test/tests/inbound_api.rs
+++ b/policy-test/tests/inbound_api.rs
@@ -1,6 +1,7 @@
 use std::time::Duration;
 
 use futures::prelude::*;
+use k8s_openapi::chrono;
 use kube::ResourceExt;
 use linkerd_policy_controller_core::{Ipv4Net, Ipv6Net};
 use linkerd_policy_controller_k8s_api as k8s;
@@ -355,6 +356,21 @@ async fn http_local_rate_limit_policy() {
                         }],
                     }]),
                 },
+                status: Some(k8s::policy::HTTPLocalRateLimitPolicyStatus {
+                    conditions: vec![k8s::Condition {
+                        last_transition_time: k8s::Time(chrono::DateTime::<chrono::Utc>::MIN_UTC),
+                        message: "".to_string(),
+                        observed_generation: None,
+                        reason: "".to_string(),
+                        status: "True".to_string(),
+                        type_: "Accepted".to_string(),
+                    }],
+                    target_ref: k8s::policy::LocalTargetRef {
+                        group: Some("policy.linkerd.io".to_string()),
+                        kind: "Server".to_string(),
+                        name: "linkerd-admin".to_string(),
+                    },
+                }),
             },
         )
         .await;