Repository: nginx/nginx-gateway-fabric

Issue:
Handle policy attachment when ancestor status slice is full
Status on policies contains a slice of Ancestors, which has a max size of 16. If the slice is full, we consider the policy unimplementable but we do not signal this to the user in any way. We effectively ignore the policy.

The spec says
>  If this slice is full, implementations MUST NOT add further entries.
Instead they MUST consider the policy unimplementable and signal that
on any related resources such as the ancestor that would be referenced
here. For example, if this list was full on BackendTLSPolicy, no
additional Gateways would be able to reference the Service targeted by
the BackendTLSPolicy.

We need to figure out a way to signal this on the ancestor resource. One way to do this would be to add a condition to the ancestor. But this may overflow the ancestor's conditions. 

Bugs found: 
- BackendTLSPolicy will fully be rejected when ancestors overflow 
- NGF policy applying will fully apply the policy to a service rather than partially apply when ancestors slice is full


### Acceptance
- BackendTLSPolicy should apply to services until the ancestors slice is full
- Sort attachments properly to that old attachments are prioritized
- Update NGF to honor policy service attachment when ancestor is not accepted and only rejected it for the ancestor
- When a policy cannot be applied by the above condition, an attempt should be made to signal on the ancestor that the policy was not applied.
- After the attempt to update status is made, a message is logged that the policy was not applied and why. Log message should also include:
  - The name of the ancestor
  - The name of the policy that was not applied

Hints from issue comments:
@kate-osborn: See this thread for more info: https://github.com/nginxinc/nginx-gateway-fabric/pull/1940#discussion_r1591749902@nginx-bot: Hi @kate-osborn!

Thanks for opening this issue!
Be sure to check out our [Contributing Guidelines](../blob/main/CONTRIBUTING.md) and the [Issue Lifecycle](../blob/main/ISSUE_LIFECYCLE.md) while you wait for someone on the team to take a look at this.

<!-- nginx-bot-comment -->

Solution patch:
diff --git a/apis/v1alpha1/clientsettingspolicy_types.go b/apis/v1alpha1/clientsettingspolicy_types.go
index a01032af..541b496b 100644
--- a/apis/v1alpha1/clientsettingspolicy_types.go
+++ b/apis/v1alpha1/clientsettingspolicy_types.go
@@ -48,8 +48,11 @@ type ClientSettingsPolicySpec struct {
 
 	// TargetRef identifies an API object to apply the policy to.
 	// Object must be in the same namespace as the policy.
+	// Support: Gateway, HTTPRoute, GRPCRoute.
 	//
-	// Support: Gateway, HTTPRoute
+	// +kubebuilder:validation:XValidation:message="TargetRef Kind must be one of: Gateway, HTTPRoute, or GRPCRoute",rule="(self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute')"
+	// +kubebuilder:validation:XValidation:message="TargetRef Group must be gateway.networking.k8s.io.",rule="(self.group=='gateway.networking.k8s.io')"
+	//nolint:lll
 	TargetRef gatewayv1alpha2.LocalPolicyTargetReference `json:"targetRef"`
 }
 
@@ -95,7 +98,11 @@ type ClientKeepAlive struct {
 
 	// Timeout defines the keep-alive timeouts for clients.
 	//
+	// +kubebuilder:validation:XValidation:message="header can only be specified if server is specified",rule="!(has(self.header) && !has(self.server))"
+	//
+	//
 	// +optional
+	//nolint:lll
 	Timeout *ClientKeepAliveTimeout `json:"timeout,omitempty"`
 }
 
diff --git a/apis/v1alpha1/policy_methods.go b/apis/v1alpha1/policy_methods.go
new file mode 100644
index 00000000..e4bb4391
--- /dev/null
+++ b/apis/v1alpha1/policy_methods.go
@@ -0,0 +1,21 @@
+package v1alpha1
+
+import (
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+)
+
+// FIXME(kate-osborn): https://github.com/nginxinc/nginx-gateway-fabric/issues/1939.
+// Figure out a way to generate these methods for all our policies.
+// These methods implement the policies.Policy interface which extends client.Object to add the following methods.
+
+func (p *ClientSettingsPolicy) GetTargetRef() v1alpha2.LocalPolicyTargetReference {
+	return p.Spec.TargetRef
+}
+
+func (p *ClientSettingsPolicy) GetPolicyStatus() v1alpha2.PolicyStatus {
+	return p.Status
+}
+
+func (p *ClientSettingsPolicy) SetPolicyStatus(status v1alpha2.PolicyStatus) {
+	p.Status = status
+}
diff --git a/charts/nginx-gateway-fabric/templates/deployment.yaml b/charts/nginx-gateway-fabric/templates/deployment.yaml
index 83aeafde..1e5fdf31 100644
--- a/charts/nginx-gateway-fabric/templates/deployment.yaml
+++ b/charts/nginx-gateway-fabric/templates/deployment.yaml
@@ -124,6 +124,8 @@ spec:
           mountPath: /etc/nginx/secrets
         - name: nginx-run
           mountPath: /var/run/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
         {{- with .Values.nginxGateway.extraVolumeMounts -}}
         {{ toYaml . | nindent 8 }}
         {{- end }}
@@ -161,6 +163,8 @@ spec:
           mountPath: /var/cache/nginx
         - name: nginx-lib
           mountPath: /var/lib/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
         {{- with .Values.nginx.extraVolumeMounts -}}
         {{ toYaml . | nindent 8 }}
         {{- end }}
@@ -195,6 +199,8 @@ spec:
         emptyDir: {}
       - name: nginx-lib
         emptyDir: {}
+      - name: nginx-includes
+        emptyDir: {}
       {{- with .Values.extraVolumes -}}
       {{ toYaml . | nindent 6 }}
       {{- end }}
diff --git a/charts/nginx-gateway-fabric/templates/rbac.yaml b/charts/nginx-gateway-fabric/templates/rbac.yaml
index 56e3adb3..cc12735a 100644
--- a/charts/nginx-gateway-fabric/templates/rbac.yaml
+++ b/charts/nginx-gateway-fabric/templates/rbac.yaml
@@ -121,6 +121,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxproxies
+  - clientsettingspolicies
   verbs:
   - list
   - watch
@@ -128,6 +129,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxgateways/status
+  - clientsettingspolicies/status
   verbs:
   - update
 {{- if .Values.nginxGateway.leaderElection.enable }}
diff --git a/config/crd/bases/gateway.nginx.org_clientsettingspolicies.yaml b/config/crd/bases/gateway.nginx.org_clientsettingspolicies.yaml
index 3f5728a7..13b5cf91 100644
--- a/config/crd/bases/gateway.nginx.org_clientsettingspolicies.yaml
+++ b/config/crd/bases/gateway.nginx.org_clientsettingspolicies.yaml
@@ -108,14 +108,15 @@ spec:
                         pattern: ^\d{1,4}(ms|s)?$
                         type: string
                     type: object
+                    x-kubernetes-validations:
+                    - message: header can only be specified if server is specified
+                      rule: '!(has(self.header) && !has(self.server))'
                 type: object
               targetRef:
                 description: |-
                   TargetRef identifies an API object to apply the policy to.
                   Object must be in the same namespace as the policy.
-
-
-                  Support: Gateway, HTTPRoute
+                  Support: Gateway, HTTPRoute, GRPCRoute.
                 properties:
                   group:
                     description: Group is the group of the target resource.
@@ -138,6 +139,12 @@ spec:
                 - kind
                 - name
                 type: object
+                x-kubernetes-validations:
+                - message: 'TargetRef Kind must be one of: Gateway, HTTPRoute, or
+                    GRPCRoute'
+                  rule: (self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute')
+                - message: TargetRef Group must be gateway.networking.k8s.io.
+                  rule: (self.group=='gateway.networking.k8s.io')
             required:
             - targetRef
             type: object
diff --git a/conformance/provisioner/static-deployment.yaml b/conformance/provisioner/static-deployment.yaml
index dc59c6a9..7f5fed61 100644
--- a/conformance/provisioner/static-deployment.yaml
+++ b/conformance/provisioner/static-deployment.yaml
@@ -76,6 +76,8 @@ spec:
           mountPath: /etc/nginx/secrets
         - name: nginx-run
           mountPath: /var/run/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       - image: ghcr.io/nginxinc/nginx-gateway-fabric/nginx:edge
         imagePullPolicy: Always
         name: nginx
@@ -106,6 +108,8 @@ spec:
           mountPath: /var/cache/nginx
         - name: nginx-lib
           mountPath: /var/lib/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       terminationGracePeriodSeconds: 30
       serviceAccountName: nginx-gateway
       shareProcessNamespace: true
@@ -125,3 +129,5 @@ spec:
         emptyDir: {}
       - name: nginx-lib
         emptyDir: {}
+      - name: nginx-includes
+        emptyDir: {}
diff --git a/deploy/crds.yaml b/deploy/crds.yaml
index 5289c855..82e793e4 100644
--- a/deploy/crds.yaml
+++ b/deploy/crds.yaml
@@ -107,14 +107,15 @@ spec:
                         pattern: ^\d{1,4}(ms|s)?$
                         type: string
                     type: object
+                    x-kubernetes-validations:
+                    - message: header can only be specified if server is specified
+                      rule: '!(has(self.header) && !has(self.server))'
                 type: object
               targetRef:
                 description: |-
                   TargetRef identifies an API object to apply the policy to.
                   Object must be in the same namespace as the policy.
-
-
-                  Support: Gateway, HTTPRoute
+                  Support: Gateway, HTTPRoute, GRPCRoute.
                 properties:
                   group:
                     description: Group is the group of the target resource.
@@ -137,6 +138,12 @@ spec:
                 - kind
                 - name
                 type: object
+                x-kubernetes-validations:
+                - message: 'TargetRef Kind must be one of: Gateway, HTTPRoute, or
+                    GRPCRoute'
+                  rule: (self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute')
+                - message: TargetRef Group must be gateway.networking.k8s.io.
+                  rule: (self.group=='gateway.networking.k8s.io')
             required:
             - targetRef
             type: object
diff --git a/deploy/manifests/nginx-gateway-experimental.yaml b/deploy/manifests/nginx-gateway-experimental.yaml
index 15db4a72..b0e068eb 100644
--- a/deploy/manifests/nginx-gateway-experimental.yaml
+++ b/deploy/manifests/nginx-gateway-experimental.yaml
@@ -103,6 +103,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxproxies
+  - clientsettingspolicies
   verbs:
   - list
   - watch
@@ -110,6 +111,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxgateways/status
+  - clientsettingspolicies/status
   verbs:
   - update
 - apiGroups:
@@ -228,6 +230,8 @@ spec:
           mountPath: /etc/nginx/secrets
         - name: nginx-run
           mountPath: /var/run/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       - image: ghcr.io/nginxinc/nginx-gateway-fabric/nginx:edge
         imagePullPolicy: Always
         name: nginx
@@ -258,6 +262,8 @@ spec:
           mountPath: /var/cache/nginx
         - name: nginx-lib
           mountPath: /var/lib/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       terminationGracePeriodSeconds: 30
       serviceAccountName: nginx-gateway
       shareProcessNamespace: true
@@ -277,6 +283,8 @@ spec:
         emptyDir: {}
       - name: nginx-lib
         emptyDir: {}
+      - name: nginx-includes
+        emptyDir: {}
 ---
 # Source: nginx-gateway-fabric/templates/gatewayclass.yaml
 apiVersion: gateway.networking.k8s.io/v1
diff --git a/deploy/manifests/nginx-gateway.yaml b/deploy/manifests/nginx-gateway.yaml
index 8dc5b265..021b2408 100644
--- a/deploy/manifests/nginx-gateway.yaml
+++ b/deploy/manifests/nginx-gateway.yaml
@@ -100,6 +100,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxproxies
+  - clientsettingspolicies
   verbs:
   - list
   - watch
@@ -107,6 +108,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxgateways/status
+  - clientsettingspolicies/status
   verbs:
   - update
 - apiGroups:
@@ -224,6 +226,8 @@ spec:
           mountPath: /etc/nginx/secrets
         - name: nginx-run
           mountPath: /var/run/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       - image: ghcr.io/nginxinc/nginx-gateway-fabric/nginx:edge
         imagePullPolicy: Always
         name: nginx
@@ -254,6 +258,8 @@ spec:
           mountPath: /var/cache/nginx
         - name: nginx-lib
           mountPath: /var/lib/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       terminationGracePeriodSeconds: 30
       serviceAccountName: nginx-gateway
       shareProcessNamespace: true
@@ -273,6 +279,8 @@ spec:
         emptyDir: {}
       - name: nginx-lib
         emptyDir: {}
+      - name: nginx-includes
+        emptyDir: {}
 ---
 # Source: nginx-gateway-fabric/templates/gatewayclass.yaml
 apiVersion: gateway.networking.k8s.io/v1
diff --git a/deploy/manifests/nginx-plus-gateway-experimental.yaml b/deploy/manifests/nginx-plus-gateway-experimental.yaml
index 81743dce..c5b61cdc 100644
--- a/deploy/manifests/nginx-plus-gateway-experimental.yaml
+++ b/deploy/manifests/nginx-plus-gateway-experimental.yaml
@@ -109,6 +109,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxproxies
+  - clientsettingspolicies
   verbs:
   - list
   - watch
@@ -116,6 +117,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxgateways/status
+  - clientsettingspolicies/status
   verbs:
   - update
 - apiGroups:
@@ -235,6 +237,8 @@ spec:
           mountPath: /etc/nginx/secrets
         - name: nginx-run
           mountPath: /var/run/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       - image: nginx-gateway-fabric/nginx-plus:edge
         imagePullPolicy: Always
         name: nginx
@@ -265,6 +269,8 @@ spec:
           mountPath: /var/cache/nginx
         - name: nginx-lib
           mountPath: /var/lib/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       terminationGracePeriodSeconds: 30
       serviceAccountName: nginx-gateway
       shareProcessNamespace: true
@@ -284,6 +290,8 @@ spec:
         emptyDir: {}
       - name: nginx-lib
         emptyDir: {}
+      - name: nginx-includes
+        emptyDir: {}
 ---
 # Source: nginx-gateway-fabric/templates/gatewayclass.yaml
 apiVersion: gateway.networking.k8s.io/v1
diff --git a/deploy/manifests/nginx-plus-gateway.yaml b/deploy/manifests/nginx-plus-gateway.yaml
index 04736bb3..b2f0a1e3 100644
--- a/deploy/manifests/nginx-plus-gateway.yaml
+++ b/deploy/manifests/nginx-plus-gateway.yaml
@@ -106,6 +106,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxproxies
+  - clientsettingspolicies
   verbs:
   - list
   - watch
@@ -113,6 +114,7 @@ rules:
   - gateway.nginx.org
   resources:
   - nginxgateways/status
+  - clientsettingspolicies/status
   verbs:
   - update
 - apiGroups:
@@ -231,6 +233,8 @@ spec:
           mountPath: /etc/nginx/secrets
         - name: nginx-run
           mountPath: /var/run/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       - image: nginx-gateway-fabric/nginx-plus:edge
         imagePullPolicy: Always
         name: nginx
@@ -261,6 +265,8 @@ spec:
           mountPath: /var/cache/nginx
         - name: nginx-lib
           mountPath: /var/lib/nginx
+        - name: nginx-includes
+          mountPath: /etc/nginx/includes
       terminationGracePeriodSeconds: 30
       serviceAccountName: nginx-gateway
       shareProcessNamespace: true
@@ -280,6 +286,8 @@ spec:
         emptyDir: {}
       - name: nginx-lib
         emptyDir: {}
+      - name: nginx-includes
+        emptyDir: {}
 ---
 # Source: nginx-gateway-fabric/templates/gatewayclass.yaml
 apiVersion: gateway.networking.k8s.io/v1
diff --git a/docs/proposals/client-settings.md b/docs/proposals/client-settings.md
index 35ca6913..5437c88f 100644
--- a/docs/proposals/client-settings.md
+++ b/docs/proposals/client-settings.md
@@ -1,7 +1,7 @@
 # Enhancement Proposal-1632: Client Settings Policy
 
 - Issue: https://github.com/nginxinc/nginx-gateway-fabric/issues/1632
-- Status: Implementable
+- Status: Completed
 
 ## Summary
 
diff --git a/examples/client-settings-policy/README.md b/examples/client-settings-policy/README.md
new file mode 100644
index 00000000..d3d7c87f
--- /dev/null
+++ b/examples/client-settings-policy/README.md
@@ -0,0 +1,3 @@
+# Client Settings Policy
+
+This directory contains YAML files of ClientSettingsPolicies.
diff --git a/examples/client-settings-policy/cafe-routes.yaml b/examples/client-settings-policy/cafe-routes.yaml
new file mode 100644
index 00000000..67927335
--- /dev/null
+++ b/examples/client-settings-policy/cafe-routes.yaml
@@ -0,0 +1,37 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: coffee
+spec:
+  parentRefs:
+  - name: gateway
+    sectionName: http
+  hostnames:
+  - "cafe.example.com"
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /coffee
+    backendRefs:
+    - name: coffee
+      port: 80
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: tea
+spec:
+  parentRefs:
+  - name: gateway
+    sectionName: http
+  hostnames:
+  - "cafe.example.com"
+  rules:
+  - matches:
+    - path:
+        type: Exact
+        value: /tea
+    backendRefs:
+    - name: tea
+      port: 80
diff --git a/examples/client-settings-policy/cafe.yaml b/examples/client-settings-policy/cafe.yaml
new file mode 100644
index 00000000..2d03ae59
--- /dev/null
+++ b/examples/client-settings-policy/cafe.yaml
@@ -0,0 +1,65 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coffee
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: coffee
+  template:
+    metadata:
+      labels:
+        app: coffee
+    spec:
+      containers:
+      - name: coffee
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: coffee
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: coffee
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: tea
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: tea
+  template:
+    metadata:
+      labels:
+        app: tea
+    spec:
+      containers:
+      - name: tea
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: tea
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: tea
diff --git a/examples/client-settings-policy/csp-gateway.yaml b/examples/client-settings-policy/csp-gateway.yaml
new file mode 100644
index 00000000..991d2b96
--- /dev/null
+++ b/examples/client-settings-policy/csp-gateway.yaml
@@ -0,0 +1,19 @@
+apiVersion: gateway.nginx.org/v1alpha1
+kind: ClientSettingsPolicy
+metadata:
+  name: gw
+  namespace: default
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: Gateway
+    name: gateway
+  body:
+    maxSize: 10m
+    timeout: 30s
+  keepAlive:
+    requests: 100
+    time: 5s
+    timeout:
+      server: 2s
+      header: 1s
diff --git a/examples/client-settings-policy/csp-grpcroute.yaml b/examples/client-settings-policy/csp-grpcroute.yaml
new file mode 100644
index 00000000..24139aa7
--- /dev/null
+++ b/examples/client-settings-policy/csp-grpcroute.yaml
@@ -0,0 +1,14 @@
+# This example should be used in conjunction with
+# the GRPC example: https://github.com/nginxinc/nginx-gateway-fabric/tree/main/examples/grpc-routing example.
+apiVersion: gateway.nginx.org/v1alpha1
+kind: ClientSettingsPolicy
+metadata:
+  name: grcp-backend-v1-route
+  namespace: default
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: GRPCRoute
+    name: backend-v1
+  body:
+    maxSize: "0" # setting to 0 disables checking of the body size
diff --git a/examples/client-settings-policy/csp-httproutes.yaml b/examples/client-settings-policy/csp-httproutes.yaml
new file mode 100644
index 00000000..1419c7b2
--- /dev/null
+++ b/examples/client-settings-policy/csp-httproutes.yaml
@@ -0,0 +1,38 @@
+apiVersion: gateway.nginx.org/v1alpha1
+kind: ClientSettingsPolicy
+metadata:
+  name: tea-route-max-body-size
+  namespace: default
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: tea
+  body:
+    maxSize: 800m
+---
+apiVersion: gateway.nginx.org/v1alpha1
+kind: ClientSettingsPolicy
+metadata:
+  name: coffee-route-max-body-size
+  namespace: default
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: coffee
+  body:
+    maxSize: 5m
+---
+apiVersion: gateway.nginx.org/v1alpha1
+kind: ClientSettingsPolicy
+metadata:
+  name: coffee-route-keepalive-requests
+  namespace: default
+spec:
+  targetRef:
+    group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: coffee
+  keepAlive:
+    requests: 100
diff --git a/examples/client-settings-policy/gateway.yaml b/examples/client-settings-policy/gateway.yaml
new file mode 100644
index 00000000..5404ac39
--- /dev/null
+++ b/examples/client-settings-policy/gateway.yaml
@@ -0,0 +1,15 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway
+spec:
+  gatewayClassName: nginx
+  listeners:
+  - name: http
+    port: 80
+    protocol: HTTP
+    hostname: "*.example.com"
+  - name: http2
+    port: 8080
+    protocol: HTTP
+    hostname: "*.example.org"
diff --git a/internal/framework/helpers/helpers.go b/internal/framework/helpers/helpers.go
index 05d23334..e7e950a1 100644
--- a/internal/framework/helpers/helpers.go
+++ b/internal/framework/helpers/helpers.go
@@ -2,7 +2,9 @@
 package helpers
 
 import (
+	"bytes"
 	"fmt"
+	"text/template"
 
 	"github.com/google/go-cmp/cmp"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -51,3 +53,37 @@ func MustCastObject[T client.Object](object client.Object) T {
 
 	panic(fmt.Errorf("unexpected object type %T", object))
 }
+
+// EqualPointers returns whether two pointers are equal.
+// Pointers are considered equal if one of the following is true:
+// - They are both nil.
+// - One is nil and the other is empty (e.g. nil string and "").
+// - They are both non-nil, and their values are the same.
+func EqualPointers[T comparable](p1, p2 *T) bool {
+	if p1 == nil && p2 == nil {
+		return true
+	}
+
+	var p1Val, p2Val T
+
+	if p1 != nil {
+		p1Val = *p1
+	}
+
+	if p2 != nil {
+		p2Val = *p2
+	}
+
+	return p1Val == p2Val
+}
+
+// MustExecuteTemplate executes the template with the given data.
+func MustExecuteTemplate(template *template.Template, data interface{}) []byte {
+	var buf bytes.Buffer
+
+	if err := template.Execute(&buf, data); err != nil {
+		panic(err)
+	}
+
+	return buf.Bytes()
+}
diff --git a/internal/framework/kinds/kinds.go b/internal/framework/kinds/kinds.go
new file mode 100644
index 00000000..e67376ea
--- /dev/null
+++ b/internal/framework/kinds/kinds.go
@@ -0,0 +1,46 @@
+package kinds
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+)
+
+// Gateway API Kinds
+const (
+	// Gateway is the Gateway Kind
+	Gateway = "Gateway"
+	// GatewayClass is the GatewayClass Kind.
+	GatewayClass = "GatewayClass"
+	// HTTPRoute is the HTTPRoute kind.
+	HTTPRoute = "HTTPRoute"
+	// GRPCRoute is the GRPCRoute kind.
+	GRPCRoute = "GRPCRoute"
+)
+
+// NGINX Gateway Fabric kinds.
+const (
+	// ClientSettingsPolicy is the ClientSettingsPolicy kind.
+	ClientSettingsPolicy = "ClientSettingsPolicy"
+	// NginxProxy is the NginxProxy kind.
+	NginxProxy = "NginxProxy"
+)
+
+// MustExtractGVK is a function that extracts the GroupVersionKind (GVK) of a client.object.
+// It will panic if the GKV cannot be extracted.
+type MustExtractGVK func(object client.Object) schema.GroupVersionKind
+
+// NewMustExtractGKV creates a new MustExtractGVK function using the scheme.
+func NewMustExtractGKV(scheme *runtime.Scheme) MustExtractGVK {
+	return func(obj client.Object) schema.GroupVersionKind {
+		gvk, err := apiutil.GVKForObject(obj, scheme)
+		if err != nil {
+			panic(fmt.Sprintf("could not extract GVK for object: %T", obj))
+		}
+
+		return gvk
+	}
+}
diff --git a/internal/mode/static/handler.go b/internal/mode/static/handler.go
index e96afb40..9bfdc1da 100644
--- a/internal/mode/static/handler.go
+++ b/internal/mode/static/handler.go
@@ -25,6 +25,7 @@ import (
 	ngxConfig "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/runtime"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
@@ -49,14 +50,14 @@ type secretStorer interface {
 
 // eventHandlerConfig holds configuration parameters for eventHandlerImpl.
 type eventHandlerConfig struct {
-	// gatewayCtlrName is the name of the NGF controller.
-	gatewayCtlrName string
-	// k8sClient is a Kubernetes API client
-	k8sClient client.Client
-	// gatewayPodConfig contains information about this Pod.
-	gatewayPodConfig ngfConfig.GatewayPodConfig
-	// usageReportConfig contains the configuration for NGINX Plus usage reporting.
-	usageReportConfig *config.UsageReportConfig
+	// nginxFileMgr is the file Manager for nginx.
+	nginxFileMgr file.Manager
+	// metricsCollector collects metrics for this controller.
+	metricsCollector handlerMetricsCollector
+	// nginxRuntimeMgr manages nginx runtime.
+	nginxRuntimeMgr runtime.Manager
+	// statusUpdater updates statuses on Kubernetes resources.
+	statusUpdater frameworkStatus.GroupUpdater
 	// usageSecret contains the Secret for the NGINX Plus reporting credentials.
 	usageSecret secretStorer
 	// processor is the state ChangeProcessor.
@@ -65,22 +66,24 @@ type eventHandlerConfig struct {
 	serviceResolver resolver.ServiceResolver
 	// generator is the nginx config generator.
 	generator ngxConfig.Generator
-	// nginxFileMgr is the file Manager for nginx.
-	nginxFileMgr file.Manager
-	// nginxRuntimeMgr manages nginx runtime.
-	nginxRuntimeMgr runtime.Manager
-	// statusUpdater updates statuses on Kubernetes resources.
-	statusUpdater frameworkStatus.GroupUpdater
-	// eventRecorder records events for Kubernetes resources.
-	eventRecorder record.EventRecorder
+	// policyConfigGenerator is the config generator for NGF policies.
+	policyConfigGenerator policies.ConfigGenerator
+	// k8sClient is a Kubernetes API client
+	k8sClient client.Client
 	// logLevelSetter is used to update the logging level.
 	logLevelSetter logLevelSetter
-	// metricsCollector collects metrics for this controller.
-	metricsCollector handlerMetricsCollector
+	// eventRecorder records events for Kubernetes resources.
+	eventRecorder record.EventRecorder
+	// usageReportConfig contains the configuration for NGINX Plus usage reporting.
+	usageReportConfig *config.UsageReportConfig
 	// nginxConfiguredOnStartChecker sets the health of the Pod to Ready once we've written out our initial config.
 	nginxConfiguredOnStartChecker *nginxConfiguredOnStartChecker
+	// gatewayPodConfig contains information about this Pod.
+	gatewayPodConfig ngfConfig.GatewayPodConfig
 	// controlConfigNSName is the NamespacedName of the NginxGateway config for this controller.
 	controlConfigNSName types.NamespacedName
+	// gatewayCtlrName is the name of the NGF controller.
+	gatewayCtlrName string
 	// updateGatewayClassStatus enables updating the status of the GatewayClass resource.
 	updateGatewayClassStatus bool
 }
@@ -194,7 +197,7 @@ func (h *eventHandlerImpl) HandleEventBatch(ctx context.Context, logger logr.Log
 		return
 	case state.EndpointsOnlyChange:
 		h.version++
-		cfg := dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.version)
+		cfg := dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.cfg.policyConfigGenerator, h.version)
 
 		h.setLatestConfiguration(&cfg)
 
@@ -205,7 +208,7 @@ func (h *eventHandlerImpl) HandleEventBatch(ctx context.Context, logger logr.Log
 		)
 	case state.ClusterStateChange:
 		h.version++
-		cfg := dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.version)
+		cfg := dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.cfg.policyConfigGenerator, h.version)
 
 		h.setLatestConfiguration(&cfg)
 
@@ -254,11 +257,13 @@ func (h *eventHandlerImpl) updateStatuses(ctx context.Context, logger logr.Logge
 	)
 
 	polReqs := status.PrepareBackendTLSPolicyRequests(graph.BackendTLSPolicies, transitionTime, h.cfg.gatewayCtlrName)
+	ngfPolReqs := status.PrepareNGFPolicyRequests(graph.NGFPolicies, transitionTime, h.cfg.gatewayCtlrName)
 
-	reqs := make([]frameworkStatus.UpdateRequest, 0, len(gcReqs)+len(routeReqs)+len(polReqs))
+	reqs := make([]frameworkStatus.UpdateRequest, 0, len(gcReqs)+len(routeReqs)+len(polReqs)+len(ngfPolReqs))
 	reqs = append(reqs, gcReqs...)
 	reqs = append(reqs, routeReqs...)
 	reqs = append(reqs, polReqs...)
+	reqs = append(reqs, ngfPolReqs...)
 
 	h.cfg.statusUpdater.UpdateGroup(ctx, groupAllExceptGateways, reqs...)
 
diff --git a/internal/mode/static/manager.go b/internal/mode/static/manager.go
index 30191296..a11e6706 100644
--- a/internal/mode/static/manager.go
+++ b/internal/mode/static/manager.go
@@ -42,6 +42,7 @@ import (
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/events"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/runnables"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/status"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/config"
@@ -50,6 +51,8 @@ import (
 	ngxvalidation "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/validation"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file"
 	ngxruntime "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/runtime"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/clientsettings"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
@@ -110,16 +113,22 @@ func StartManager(cfg config.Config) error {
 		int32(cfg.HealthConfig.Port):  "HealthPort",
 	}
 
+	mustExtractGVK := kinds.NewMustExtractGKV(scheme)
+
+	genericValidator := ngxvalidation.GenericValidator{}
+	policyManager := createPolicyManager(mustExtractGVK, genericValidator)
+
 	processor := state.NewChangeProcessorImpl(state.ChangeProcessorConfig{
 		GatewayCtlrName:  cfg.GatewayCtlrName,
 		GatewayClassName: cfg.GatewayClassName,
 		Logger:           cfg.Logger.WithName("changeProcessor"),
 		Validators: validation.Validators{
 			HTTPFieldsValidator: ngxvalidation.HTTPValidator{},
-			GenericValidator:    ngxvalidation.GenericValidator{},
+			GenericValidator:    genericValidator,
+			PolicyValidator:     policyManager,
 		},
 		EventRecorder:  recorder,
-		Scheme:         scheme,
+		MustExtractGVK: mustExtractGVK,
 		ProtectedPorts: protectedPorts,
 	})
 
@@ -221,6 +230,7 @@ func StartManager(cfg config.Config) error {
 		usageSecret:                   usageSecret,
 		gatewayCtlrName:               cfg.GatewayCtlrName,
 		updateGatewayClassStatus:      cfg.UpdateGatewayClassStatus,
+		policyConfigGenerator:         policyManager,
 	})
 
 	objects, objectLists := prepareFirstEventBatchPreparerArgs(
@@ -272,6 +282,21 @@ func StartManager(cfg config.Config) error {
 	return mgr.Start(ctx)
 }
 
+func createPolicyManager(
+	mustExtractGVK kinds.MustExtractGVK,
+	validator validation.GenericValidator,
+) *policies.Manager {
+	cfgs := []policies.ManagerConfig{
+		{
+			GVK:       mustExtractGVK(&ngfAPI.ClientSettingsPolicy{}),
+			Validator: clientsettings.NewValidator(validator),
+			Generator: clientsettings.Generate,
+		},
+	}
+
+	return policies.NewManager(mustExtractGVK, cfgs...)
+}
+
 func createManager(cfg config.Config, nginxChecker *nginxConfiguredOnStartChecker) (manager.Manager, error) {
 	options := manager.Options{
 		Scheme:  scheme,
@@ -340,9 +365,12 @@ func registerControllers(
 		{
 			objectType: &gatewayv1.GatewayClass{},
 			options: []controller.Option{
-				controller.WithK8sPredicate(k8spredicate.And(
-					k8spredicate.GenerationChangedPredicate{},
-					predicate.GatewayClassPredicate{ControllerName: cfg.GatewayCtlrName})),
+				controller.WithK8sPredicate(
+					k8spredicate.And(
+						k8spredicate.GenerationChangedPredicate{},
+						predicate.GatewayClassPredicate{ControllerName: cfg.GatewayCtlrName},
+					),
+				),
 			},
 		},
 		{
@@ -427,6 +455,12 @@ func registerControllers(
 				controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
 			},
 		},
+		{
+			objectType: &ngfAPI.ClientSettingsPolicy{},
+			options: []controller.Option{
+				controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
+			},
+		},
 	}
 
 	if cfg.ExperimentalFeatures {
@@ -607,6 +641,7 @@ func prepareFirstEventBatchPreparerArgs(
 		&gatewayv1beta1.ReferenceGrantList{},
 		&ngfAPI.NginxProxyList{},
 		&gatewayv1.GRPCRouteList{},
+		&ngfAPI.ClientSettingsPolicyList{},
 		partialObjectMetadataList,
 	}
 
diff --git a/internal/mode/static/nginx/config/base_http_config.go b/internal/mode/static/nginx/config/base_http_config.go
index bcd8ae94..abec4464 100644
--- a/internal/mode/static/nginx/config/base_http_config.go
+++ b/internal/mode/static/nginx/config/base_http_config.go
@@ -3,6 +3,7 @@ package config
 import (
 	gotemplate "text/template"
 
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
 )
 
@@ -11,7 +12,7 @@ var baseHTTPTemplate = gotemplate.Must(gotemplate.New("baseHttp").Parse(baseHTTP
 func executeBaseHTTPConfig(conf dataplane.Configuration) []executeResult {
 	result := executeResult{
 		dest: httpConfigFile,
-		data: execute(baseHTTPTemplate, conf.BaseHTTPConfig),
+		data: helpers.MustExecuteTemplate(baseHTTPTemplate, conf.BaseHTTPConfig),
 	}
 
 	return []executeResult{result}
diff --git a/internal/mode/static/nginx/config/execute.go b/internal/mode/static/nginx/config/execute.go
deleted file mode 100644
index eabc4774..00000000
--- a/internal/mode/static/nginx/config/execute.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package config
-
-import (
-	"bytes"
-	"text/template"
-)
-
-// executes the template with the given data.
-func execute(template *template.Template, data interface{}) []byte {
-	var buf bytes.Buffer
-
-	if err := template.Execute(&buf, data); err != nil {
-		panic(err)
-	}
-
-	return buf.Bytes()
-}
diff --git a/internal/mode/static/nginx/config/generator.go b/internal/mode/static/nginx/config/generator.go
index 9d5c6e8f..a0509194 100644
--- a/internal/mode/static/nginx/config/generator.go
+++ b/internal/mode/static/nginx/config/generator.go
@@ -23,6 +23,9 @@ const (
 	// secretsFolder is the folder where secrets (like TLS certs/keys) are stored.
 	secretsFolder = configFolder + "/secrets"
 
+	// includesFolder is the folder where are all include files are stored.
+	includesFolder = configFolder + "/includes"
+
 	// httpConfigFile is the path to the configuration file with HTTP configuration.
 	httpConfigFile = httpFolder + "/http.conf"
 
@@ -37,7 +40,7 @@ const (
 )
 
 // ConfigFolders is a list of folders where NGINX configuration files are stored.
-var ConfigFolders = []string{httpFolder, secretsFolder, modulesIncludesFolder}
+var ConfigFolders = []string{httpFolder, secretsFolder, includesFolder, modulesIncludesFolder}
 
 // Generator generates NGINX configuration files.
 // This interface is used for testing purposes only.
diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go
index cae46e1b..19e7db22 100644
--- a/internal/mode/static/nginx/config/http/config.go
+++ b/internal/mode/static/nginx/config/http/config.go
@@ -5,10 +5,11 @@ type Server struct {
 	SSL           *SSL
 	ServerName    string
 	Locations     []Location
+	Includes      []string
+	Port          int32
 	IsDefaultHTTP bool
 	IsDefaultSSL  bool
 	GRPC          bool
-	Port          int32
 }
 
 // Location holds all configuration for an HTTP location.
@@ -17,11 +18,12 @@ type Location struct {
 	ProxyPass       string
 	HTTPMatchKey    string
 	HTTPMatchVar    string
-	Rewrites        []string
 	ProxySetHeaders []Header
 	ProxySSLVerify  *ProxySSLVerify
 	Return          *Return
 	ResponseHeaders ResponseHeaders
+	Rewrites        []string
+	Includes        []string
 	GRPC            bool
 }
 
diff --git a/internal/mode/static/nginx/config/maps.go b/internal/mode/static/nginx/config/maps.go
index a4d6c419..97f5486a 100644
--- a/internal/mode/static/nginx/config/maps.go
+++ b/internal/mode/static/nginx/config/maps.go
@@ -4,6 +4,7 @@ import (
 	"strings"
 	gotemplate "text/template"
 
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
 )
@@ -14,8 +15,9 @@ func executeMaps(conf dataplane.Configuration) []executeResult {
 	maps := buildAddHeaderMaps(append(conf.HTTPServers, conf.SSLServers...))
 	result := executeResult{
 		dest: httpConfigFile,
-		data: execute(mapsTemplate, maps),
+		data: helpers.MustExecuteTemplate(mapsTemplate, maps),
 	}
+
 	return []executeResult{result}
 }
 
diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go
index b67b98af..cc385449 100644
--- a/internal/mode/static/nginx/config/servers.go
+++ b/internal/mode/static/nginx/config/servers.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 	gotemplate "text/template"
 
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
 )
@@ -61,7 +62,7 @@ func executeServers(conf dataplane.Configuration) []executeResult {
 
 	serverResult := executeResult{
 		dest: httpConfigFile,
-		data: execute(serversTemplate, servers),
+		data: helpers.MustExecuteTemplate(serversTemplate, servers),
 	}
 
 	// create httpMatchPair conf
@@ -76,7 +77,68 @@ func executeServers(conf dataplane.Configuration) []executeResult {
 		data: httpMatchConf,
 	}
 
-	return []executeResult{serverResult, httpMatchResult}
+	additionFileResults := createAdditionFileResults(conf)
+
+	allResults := make([]executeResult, 0, len(additionFileResults)+2)
+	allResults = append(allResults, additionFileResults...)
+	allResults = append(allResults, serverResult, httpMatchResult)
+
+	return allResults
+}
+
+func createAdditionFileResults(conf dataplane.Configuration) []executeResult {
+	uniqueAdditions := make(map[string][]byte)
+
+	findUniqueAdditionsForServer := func(server dataplane.VirtualServer) {
+		for _, add := range server.Additions {
+			uniqueAdditions[createAdditionFileName(add)] = add.Bytes
+		}
+
+		for _, pr := range server.PathRules {
+			for _, mr := range pr.MatchRules {
+				for _, add := range mr.Additions {
+					uniqueAdditions[createAdditionFileName(add)] = add.Bytes
+				}
+			}
+		}
+	}
+
+	for _, s := range conf.HTTPServers {
+		findUniqueAdditionsForServer(s)
+	}
+
+	for _, s := range conf.SSLServers {
+		findUniqueAdditionsForServer(s)
+	}
+
+	results := make([]executeResult, 0, len(uniqueAdditions))
+
+	for filename, contents := range uniqueAdditions {
+		results = append(results, executeResult{
+			dest: filename,
+			data: contents,
+		})
+	}
+
+	return results
+}
+
+func createAdditionFileName(addition dataplane.Addition) string {
+	return fmt.Sprintf("%s/%s.conf", includesFolder, addition.Identifier)
+}
+
+func createIncludes(additions []dataplane.Addition) []string {
+	if len(additions) == 0 {
+		return nil
+	}
+
+	includes := make([]string, 0, len(additions))
+
+	for _, addition := range additions {
+		includes = append(includes, createAdditionFileName(addition))
+	}
+
+	return includes
 }
 
 func createServers(httpServers, sslServers []dataplane.VirtualServer) ([]http.Server, httpMatchPairs) {
@@ -117,6 +179,7 @@ func createSSLServer(virtualServer dataplane.VirtualServer, serverID int) (http.
 		Locations: locs,
 		Port:      virtualServer.Port,
 		GRPC:      grpc,
+		Includes:  createIncludes(virtualServer.Additions),
 	}, matchPairs
 }
 
@@ -135,6 +198,7 @@ func createServer(virtualServer dataplane.VirtualServer, serverID int) (http.Ser
 		Locations:  locs,
 		Port:       virtualServer.Port,
 		GRPC:       grpc,
+		Includes:   createIncludes(virtualServer.Additions),
 	}, matchPairs
 }
 
@@ -169,12 +233,20 @@ func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Loca
 
 		for matchRuleIdx, r := range rule.MatchRules {
 			buildLocations := extLocations
+
 			if len(rule.MatchRules) != 1 || !isPathOnlyMatch(r.Match) {
 				intLocation, match := initializeInternalLocation(pathRuleIdx, matchRuleIdx, r.Match)
 				buildLocations = []http.Location{intLocation}
 				matches = append(matches, match)
 			}
 
+			includes := createIncludes(r.Additions)
+
+			// buildLocations will either contain the extLocations OR the intLocation.
+			// If it contains the intLocation, the extLocations will be added to the final locations after we loop
+			// through all the MatchRules.
+			// It is safe to modify the buildLocations here by adding includes and filters.
+			buildLocations = updateLocationsForIncludes(buildLocations, includes)
 			buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, server.Port, rule.Path, rule.GRPC)
 			locs = append(locs, buildLocations...)
 		}
@@ -203,6 +275,14 @@ func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Loca
 	return locs, matchPairs, grpc
 }
 
+func updateLocationsForIncludes(locations []http.Location, includes []string) []http.Location {
+	for i := range locations {
+		locations[i].Includes = includes
+	}
+
+	return locations
+}
+
 // pathAndTypeMap contains a map of paths and any path types defined for that path
 // for example, {/foo: {exact: {}, prefix: {}}}
 type pathAndTypeMap map[string]map[dataplane.PathType]struct{}
diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go
index 12ebb602..c12961be 100644
--- a/internal/mode/static/nginx/config/servers_template.go
+++ b/internal/mode/static/nginx/config/servers_template.go
@@ -32,9 +32,17 @@ server {
 
     server_name {{ $s.ServerName }};
 
+    {{- range $i := $s.Includes }}
+    include {{ $i }};
+    {{ end -}}
+
         {{ range $l := $s.Locations }}
     location {{ $l.Path }} {
-        {{- range $r := $l.Rewrites }}
+        {{- range $i := $l.Includes }}
+        include {{ $i }};
+        {{- end -}}
+
+        {{ range $r := $l.Rewrites }}
         rewrite {{ $r }};
         {{- end }}
 
diff --git a/internal/mode/static/nginx/config/split_clients.go b/internal/mode/static/nginx/config/split_clients.go
index 7d74ad18..0cc050e7 100644
--- a/internal/mode/static/nginx/config/split_clients.go
+++ b/internal/mode/static/nginx/config/split_clients.go
@@ -5,6 +5,7 @@ import (
 	"math"
 	gotemplate "text/template"
 
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
 )
@@ -16,7 +17,7 @@ func executeSplitClients(conf dataplane.Configuration) []executeResult {
 
 	result := executeResult{
 		dest: httpConfigFile,
-		data: execute(splitClientsTemplate, splitClients),
+		data: helpers.MustExecuteTemplate(splitClientsTemplate, splitClients),
 	}
 
 	return []executeResult{result}
diff --git a/internal/mode/static/nginx/config/telemetry.go b/internal/mode/static/nginx/config/telemetry.go
index fef1e4ec..719fad9f 100644
--- a/internal/mode/static/nginx/config/telemetry.go
+++ b/internal/mode/static/nginx/config/telemetry.go
@@ -3,6 +3,7 @@ package config
 import (
 	gotemplate "text/template"
 
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
 )
 
@@ -12,7 +13,7 @@ func executeTelemetry(conf dataplane.Configuration) []executeResult {
 	if conf.Telemetry.Endpoint != "" {
 		result := executeResult{
 			dest: httpConfigFile,
-			data: execute(otelTemplate, conf.Telemetry),
+			data: helpers.MustExecuteTemplate(otelTemplate, conf.Telemetry),
 		}
 
 		return []executeResult{result}
diff --git a/internal/mode/static/nginx/config/upstreams.go b/internal/mode/static/nginx/config/upstreams.go
index 9c9c2b71..37d344e9 100644
--- a/internal/mode/static/nginx/config/upstreams.go
+++ b/internal/mode/static/nginx/config/upstreams.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	gotemplate "text/template"
 
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
 )
@@ -28,8 +29,9 @@ func (g GeneratorImpl) executeUpstreams(conf dataplane.Configuration) []executeR
 
 	result := executeResult{
 		dest: httpConfigFile,
-		data: execute(upstreamsTemplate, upstreams),
+		data: helpers.MustExecuteTemplate(upstreamsTemplate, upstreams),
 	}
+
 	return []executeResult{result}
 }
 
diff --git a/internal/mode/static/nginx/config/validation/generic.go b/internal/mode/static/nginx/config/validation/generic.go
index bac2ecf8..73c6f010 100644
--- a/internal/mode/static/nginx/config/validation/generic.go
+++ b/internal/mode/static/nginx/config/validation/generic.go
@@ -59,6 +59,29 @@ func (GenericValidator) ValidateNginxDuration(duration string) error {
 	return nil
 }
 
+const (
+	sizeStringFmt    = `^\d{1,4}(k|m|g)?$`
+	sizeStringErrMsg = "must contain a number. May be followed by 'k', 'm', or 'g', otherwise bytes are assumed"
+)
+
+var sizeStringFmtRegexp = regexp.MustCompile("^" + sizeStringFmt + "$")
+
+// ValidateNginxSize validates a size string that nginx can understand.
+func (GenericValidator) ValidateNginxSize(size string) error {
+	if !sizeStringFmtRegexp.MatchString(size) {
+		examples := []string{
+			"1024",
+			"8k",
+			"20m",
+			"1g",
+		}
+
+		return errors.New(k8svalidation.RegexError(sizeStringFmt, sizeStringErrMsg, examples...))
+	}
+
+	return nil
+}
+
 const (
 	//nolint:lll
 	endpointStringFmt    = `(?:http?:\/\/)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*(?::\d{1,5})?`
diff --git a/internal/mode/static/nginx/config/version.go b/internal/mode/static/nginx/config/version.go
index 494a3f7d..5baa7f24 100644
--- a/internal/mode/static/nginx/config/version.go
+++ b/internal/mode/static/nginx/config/version.go
@@ -2,10 +2,12 @@ package config
 
 import (
 	gotemplate "text/template"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 )
 
 var versionTemplate = gotemplate.Must(gotemplate.New("version").Parse(versionTemplateText))
 
 func executeVersion(version int) []byte {
-	return execute(versionTemplate, version)
+	return helpers.MustExecuteTemplate(versionTemplate, version)
 }
diff --git a/internal/mode/static/policies/clientsettings/generator.go b/internal/mode/static/policies/clientsettings/generator.go
new file mode 100644
index 00000000..e7f50ef8
--- /dev/null
+++ b/internal/mode/static/policies/clientsettings/generator.go
@@ -0,0 +1,44 @@
+package clientsettings
+
+import (
+	"text/template"
+
+	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+)
+
+var tmpl = template.Must(template.New("client settings policy").Parse(clientSettingsTemplate))
+
+const clientSettingsTemplate = `
+{{- if .Body }}
+	{{- if .Body.MaxSize }}
+client_max_body_size {{ .Body.MaxSize }};
+	{{- end }}
+	{{- if .Body.Timeout }}
+client_body_timeout {{ .Body.Timeout }};
+	{{- end }}
+{{- end }}
+{{- if .KeepAlive }}
+	{{- if .KeepAlive.Requests }}
+keepalive_requests {{ .KeepAlive.Requests }};
+	{{- end }}
+	{{- if .KeepAlive.Time }}
+keepalive_time {{ .KeepAlive.Time }};
+	{{- end }}
+    {{- if .KeepAlive.Timeout }}
+        {{- if and .KeepAlive.Timeout.Server .KeepAlive.Timeout.Header }}
+keepalive_timeout {{ .KeepAlive.Timeout.Server }} {{ .KeepAlive.Timeout.Header }};
+        {{- else if .KeepAlive.Timeout.Server }}
+keepalive_timeout {{ .KeepAlive.Timeout.Server }};
+        {{- end }}
+    {{- end }}
+{{- end }}
+`
+
+// Generate generates configuration as []byte for a ClientSettingsPolicy.
+func Generate(policy policies.Policy) []byte {
+	csp := helpers.MustCastObject[*ngfAPI.ClientSettingsPolicy](policy)
+
+	return helpers.MustExecuteTemplate(tmpl, csp.Spec)
+}
diff --git a/internal/mode/static/policies/clientsettings/validator.go b/internal/mode/static/policies/clientsettings/validator.go
new file mode 100644
index 00000000..efe86304
--- /dev/null
+++ b/internal/mode/static/policies/clientsettings/validator.go
@@ -0,0 +1,196 @@
+package clientsettings
+
+import (
+	"slices"
+
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
+)
+
+// Validator validates a ClientSettingsPolicy.
+// Implements policies.Validator interface.
+type Validator struct {
+	genericValidator validation.GenericValidator
+}
+
+// NewValidator returns a new instance of Validator.
+func NewValidator(genericValidator validation.GenericValidator) *Validator {
+	return &Validator{genericValidator: genericValidator}
+}
+
+// Validate validates the spec of a ClientSettingsPolicy.
+func (v *Validator) Validate(policy policies.Policy) error {
+	csp := helpers.MustCastObject[*ngfAPI.ClientSettingsPolicy](policy)
+
+	if err := validateTargetRef(csp.Spec.TargetRef); err != nil {
+		return err
+	}
+
+	return v.validateSettings(csp.Spec)
+}
+
+// Conflicts returns true if the two ClientSettingsPolicies conflict.
+func (v *Validator) Conflicts(polA, polB policies.Policy) bool {
+	cspA := helpers.MustCastObject[*ngfAPI.ClientSettingsPolicy](polA)
+	cspB := helpers.MustCastObject[*ngfAPI.ClientSettingsPolicy](polB)
+
+	return conflicts(cspA.Spec, cspB.Spec)
+}
+
+func conflicts(a, b ngfAPI.ClientSettingsPolicySpec) bool {
+	if a.Body != nil && b.Body != nil {
+		if a.Body.Timeout != nil && b.Body.Timeout != nil {
+			return true
+		}
+
+		if a.Body.MaxSize != nil && b.Body.MaxSize != nil {
+			return true
+		}
+	}
+
+	if a.KeepAlive != nil && b.KeepAlive != nil {
+		if a.KeepAlive.Requests != nil && b.KeepAlive.Requests != nil {
+			return true
+		}
+
+		if a.KeepAlive.Time != nil && b.KeepAlive.Time != nil {
+			return true
+		}
+
+		if a.KeepAlive.Timeout != nil && b.KeepAlive.Timeout != nil {
+			return true
+		}
+
+	}
+
+	return false
+}
+
+func validateTargetRef(ref v1alpha2.LocalPolicyTargetReference) error {
+	basePath := field.NewPath("spec").Child("targetRef")
+
+	if ref.Group != gatewayv1.GroupName {
+		path := basePath.Child("group")
+
+		return field.NotSupported(
+			path,
+			ref.Group,
+			[]string{gatewayv1.GroupName},
+		)
+	}
+
+	supportedKinds := []gatewayv1.Kind{kinds.Gateway, kinds.HTTPRoute, kinds.GRPCRoute}
+
+	if !slices.Contains(supportedKinds, ref.Kind) {
+		path := basePath.Child("kind")
+
+		return field.NotSupported(
+			path,
+			ref.Kind,
+			supportedKinds,
+		)
+	}
+
+	return nil
+}
+
+// validateSettings performs validation on fields in the spec that are vulnerable to code injection.
+// For all other fields, we rely on the CRD validation.
+func (v *Validator) validateSettings(spec ngfAPI.ClientSettingsPolicySpec) error {
+	var allErrs field.ErrorList
+	fieldPath := field.NewPath("spec")
+
+	if spec.Body != nil {
+		allErrs = append(allErrs, v.validateClientBody(*spec.Body, fieldPath.Child("body"))...)
+	}
+
+	if spec.KeepAlive != nil {
+		allErrs = append(allErrs, v.validateClientKeepAlive(*spec.KeepAlive, fieldPath.Child("keepAlive"))...)
+	}
+
+	return allErrs.ToAggregate()
+}
+
+func (v *Validator) validateClientBody(body ngfAPI.ClientBody, fieldPath *field.Path) field.ErrorList {
+	var allErrs field.ErrorList
+	if body.Timeout != nil {
+		if err := v.genericValidator.ValidateNginxDuration(string(*body.Timeout)); err != nil {
+			path := fieldPath.Child("timeout")
+
+			allErrs = append(allErrs, field.Invalid(path, body.Timeout, err.Error()))
+		}
+	}
+
+	if body.MaxSize != nil {
+		if err := v.genericValidator.ValidateNginxSize(string(*body.MaxSize)); err != nil {
+			path := fieldPath.Child("maxSize")
+
+			allErrs = append(allErrs, field.Invalid(path, body.MaxSize, err.Error()))
+		}
+	}
+
+	return allErrs
+}
+
+func (v *Validator) validateClientKeepAlive(keepAlive ngfAPI.ClientKeepAlive, fieldPath *field.Path) field.ErrorList {
+	var allErrs field.ErrorList
+
+	if keepAlive.Time != nil {
+		if err := v.genericValidator.ValidateNginxDuration(string(*keepAlive.Time)); err != nil {
+			path := fieldPath.Child("time")
+
+			allErrs = append(allErrs, field.Invalid(path, *keepAlive.Time, err.Error()))
+		}
+	}
+
+	if keepAlive.Timeout != nil {
+		timeout := keepAlive.Timeout
+
+		if timeout.Server != nil {
+			if err := v.genericValidator.ValidateNginxDuration(string(*timeout.Server)); err != nil {
+				path := fieldPath.Child("timeout").Child("server")
+
+				allErrs = append(
+					allErrs,
+					field.Invalid(path, *keepAlive.Timeout.Server, err.Error()),
+				)
+			}
+		}
+
+		if timeout.Header != nil {
+			if err := v.genericValidator.ValidateNginxDuration(string(*timeout.Header)); err != nil {
+				path := fieldPath.Child("timeout").Child("header")
+
+				allErrs = append(
+					allErrs,
+					field.Invalid(path, *keepAlive.Timeout.Header, err.Error()),
+				)
+			}
+		}
+
+		// This is a special case. The keepalive_timeout directive takes two parameters:
+		// keepalive_timeout server [header], where header is optional. If header is provided and server is not,
+		// we can't properly configure the directive.
+		if keepAlive.Timeout.Header != nil && keepAlive.Timeout.Server == nil {
+			path := fieldPath.Child("timeout")
+
+			allErrs = append(
+				allErrs,
+				field.Invalid(
+					path,
+					nil,
+					"server timeout must be set if header timeout is set",
+				),
+			)
+		}
+	}
+
+	return allErrs
+}
diff --git a/internal/mode/static/policies/manager.go b/internal/mode/static/policies/manager.go
new file mode 100644
index 00000000..f1f7a15f
--- /dev/null
+++ b/internal/mode/static/policies/manager.go
@@ -0,0 +1,97 @@
+package policies
+
+//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+)
+
+// GenerateFunc generates config as []byte for an NGF Policy.
+type GenerateFunc func(policy Policy) []byte
+
+// Validator validates an NGF Policy.
+//
+//counterfeiter:generate . Validator
+type Validator interface {
+	// Validate validates an NGF Policy.
+	Validate(policy Policy) error
+	// Conflicts returns true if the two Policies conflict.
+	Conflicts(a, b Policy) bool
+}
+
+// Manager manages the validators and generators for NGF Policies.
+type Manager struct {
+	validators     map[schema.GroupVersionKind]Validator
+	generators     map[schema.GroupVersionKind]GenerateFunc
+	mustExtractGVK kinds.MustExtractGVK
+}
+
+// ManagerConfig contains the config to register a Policy with the Manager.
+type ManagerConfig struct {
+	// Validator is the Validator for the Policy.
+	Validator Validator
+	// Generate is the GenerateFunc for the Policy.
+	Generator GenerateFunc
+	// GVK is the GroupVersionKind of the Policy.
+	GVK schema.GroupVersionKind
+}
+
+// NewManager returns a new Manager.
+// Implements dataplane.ConfigGenerator and validation.PolicyValidator.
+func NewManager(
+	mustExtractGVK kinds.MustExtractGVK,
+	configs ...ManagerConfig,
+) *Manager {
+	v := &Manager{
+		validators:     make(map[schema.GroupVersionKind]Validator),
+		generators:     make(map[schema.GroupVersionKind]GenerateFunc),
+		mustExtractGVK: mustExtractGVK,
+	}
+
+	for _, cfg := range configs {
+		v.validators[cfg.GVK] = cfg.Validator
+		v.generators[cfg.GVK] = cfg.Generator
+	}
+
+	return v
+}
+
+// Generate generates config for the policy as a byte array.
+func (m *Manager) Generate(policy Policy) []byte {
+	gvk := m.mustExtractGVK(policy)
+
+	generate, ok := m.generators[gvk]
+	if !ok {
+		panic(fmt.Sprintf("no generate function registered for policy %T", policy))
+	}
+
+	return generate(policy)
+}
+
+// Validate validates the policy.
+func (m *Manager) Validate(policy Policy) error {
+	gvk := m.mustExtractGVK(policy)
+
+	validator, ok := m.validators[gvk]
+	if !ok {
+		panic(fmt.Sprintf("no validator registered for policy %T", policy))
+	}
+
+	return validator.Validate(policy)
+}
+
+// Conflicts returns true if the policies conflict.
+func (m *Manager) Conflicts(polA, polB Policy) bool {
+	gvk := m.mustExtractGVK(polA)
+
+	validator, ok := m.validators[gvk]
+	if !ok {
+		panic(fmt.Sprintf("no validator registered for policy %T", polA))
+	}
+
+	return validator.Conflicts(polA, polB)
+}
diff --git a/internal/mode/static/policies/policiesfakes/fake_config_generator.go b/internal/mode/static/policies/policiesfakes/fake_config_generator.go
new file mode 100644
index 00000000..92680ac2
--- /dev/null
+++ b/internal/mode/static/policies/policiesfakes/fake_config_generator.go
@@ -0,0 +1,111 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package policiesfakes
+
+import (
+	"sync"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+)
+
+type FakeConfigGenerator struct {
+	GenerateStub        func(policies.Policy) []byte
+	generateMutex       sync.RWMutex
+	generateArgsForCall []struct {
+		arg1 policies.Policy
+	}
+	generateReturns struct {
+		result1 []byte
+	}
+	generateReturnsOnCall map[int]struct {
+		result1 []byte
+	}
+	invocations      map[string][][]interface{}
+	invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeConfigGenerator) Generate(arg1 policies.Policy) []byte {
+	fake.generateMutex.Lock()
+	ret, specificReturn := fake.generateReturnsOnCall[len(fake.generateArgsForCall)]
+	fake.generateArgsForCall = append(fake.generateArgsForCall, struct {
+		arg1 policies.Policy
+	}{arg1})
+	stub := fake.GenerateStub
+	fakeReturns := fake.generateReturns
+	fake.recordInvocation("Generate", []interface{}{arg1})
+	fake.generateMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeConfigGenerator) GenerateCallCount() int {
+	fake.generateMutex.RLock()
+	defer fake.generateMutex.RUnlock()
+	return len(fake.generateArgsForCall)
+}
+
+func (fake *FakeConfigGenerator) GenerateCalls(stub func(policies.Policy) []byte) {
+	fake.generateMutex.Lock()
+	defer fake.generateMutex.Unlock()
+	fake.GenerateStub = stub
+}
+
+func (fake *FakeConfigGenerator) GenerateArgsForCall(i int) policies.Policy {
+	fake.generateMutex.RLock()
+	defer fake.generateMutex.RUnlock()
+	argsForCall := fake.generateArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeConfigGenerator) GenerateReturns(result1 []byte) {
+	fake.generateMutex.Lock()
+	defer fake.generateMutex.Unlock()
+	fake.GenerateStub = nil
+	fake.generateReturns = struct {
+		result1 []byte
+	}{result1}
+}
+
+func (fake *FakeConfigGenerator) GenerateReturnsOnCall(i int, result1 []byte) {
+	fake.generateMutex.Lock()
+	defer fake.generateMutex.Unlock()
+	fake.GenerateStub = nil
+	if fake.generateReturnsOnCall == nil {
+		fake.generateReturnsOnCall = make(map[int]struct {
+			result1 []byte
+		})
+	}
+	fake.generateReturnsOnCall[i] = struct {
+		result1 []byte
+	}{result1}
+}
+
+func (fake *FakeConfigGenerator) Invocations() map[string][][]interface{} {
+	fake.invocationsMutex.RLock()
+	defer fake.invocationsMutex.RUnlock()
+	fake.generateMutex.RLock()
+	defer fake.generateMutex.RUnlock()
+	copiedInvocations := map[string][][]interface{}{}
+	for key, value := range fake.invocations {
+		copiedInvocations[key] = value
+	}
+	return copiedInvocations
+}
+
+func (fake *FakeConfigGenerator) recordInvocation(key string, args []interface{}) {
+	fake.invocationsMutex.Lock()
+	defer fake.invocationsMutex.Unlock()
+	if fake.invocations == nil {
+		fake.invocations = map[string][][]interface{}{}
+	}
+	if fake.invocations[key] == nil {
+		fake.invocations[key] = [][]interface{}{}
+	}
+	fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ policies.ConfigGenerator = new(FakeConfigGenerator)
diff --git a/internal/mode/static/policies/policiesfakes/fake_object_kind.go b/internal/mode/static/policies/policiesfakes/fake_object_kind.go
new file mode 100644
index 00000000..02f55e09
--- /dev/null
+++ b/internal/mode/static/policies/policiesfakes/fake_object_kind.go
@@ -0,0 +1,141 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package policiesfakes
+
+import (
+	"sync"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+type FakeObjectKind struct {
+	GroupVersionKindStub        func() schema.GroupVersionKind
+	groupVersionKindMutex       sync.RWMutex
+	groupVersionKindArgsForCall []struct {
+	}
+	groupVersionKindReturns struct {
+		result1 schema.GroupVersionKind
+	}
+	groupVersionKindReturnsOnCall map[int]struct {
+		result1 schema.GroupVersionKind
+	}
+	SetGroupVersionKindStub        func(schema.GroupVersionKind)
+	setGroupVersionKindMutex       sync.RWMutex
+	setGroupVersionKindArgsForCall []struct {
+		arg1 schema.GroupVersionKind
+	}
+	invocations      map[string][][]interface{}
+	invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeObjectKind) GroupVersionKind() schema.GroupVersionKind {
+	fake.groupVersionKindMutex.Lock()
+	ret, specificReturn := fake.groupVersionKindReturnsOnCall[len(fake.groupVersionKindArgsForCall)]
+	fake.groupVersionKindArgsForCall = append(fake.groupVersionKindArgsForCall, struct {
+	}{})
+	stub := fake.GroupVersionKindStub
+	fakeReturns := fake.groupVersionKindReturns
+	fake.recordInvocation("GroupVersionKind", []interface{}{})
+	fake.groupVersionKindMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeObjectKind) GroupVersionKindCallCount() int {
+	fake.groupVersionKindMutex.RLock()
+	defer fake.groupVersionKindMutex.RUnlock()
+	return len(fake.groupVersionKindArgsForCall)
+}
+
+func (fake *FakeObjectKind) GroupVersionKindCalls(stub func() schema.GroupVersionKind) {
+	fake.groupVersionKindMutex.Lock()
+	defer fake.groupVersionKindMutex.Unlock()
+	fake.GroupVersionKindStub = stub
+}
+
+func (fake *FakeObjectKind) GroupVersionKindReturns(result1 schema.GroupVersionKind) {
+	fake.groupVersionKindMutex.Lock()
+	defer fake.groupVersionKindMutex.Unlock()
+	fake.GroupVersionKindStub = nil
+	fake.groupVersionKindReturns = struct {
+		result1 schema.GroupVersionKind
+	}{result1}
+}
+
+func (fake *FakeObjectKind) GroupVersionKindReturnsOnCall(i int, result1 schema.GroupVersionKind) {
+	fake.groupVersionKindMutex.Lock()
+	defer fake.groupVersionKindMutex.Unlock()
+	fake.GroupVersionKindStub = nil
+	if fake.groupVersionKindReturnsOnCall == nil {
+		fake.groupVersionKindReturnsOnCall = make(map[int]struct {
+			result1 schema.GroupVersionKind
+		})
+	}
+	fake.groupVersionKindReturnsOnCall[i] = struct {
+		result1 schema.GroupVersionKind
+	}{result1}
+}
+
+func (fake *FakeObjectKind) SetGroupVersionKind(arg1 schema.GroupVersionKind) {
+	fake.setGroupVersionKindMutex.Lock()
+	fake.setGroupVersionKindArgsForCall = append(fake.setGroupVersionKindArgsForCall, struct {
+		arg1 schema.GroupVersionKind
+	}{arg1})
+	stub := fake.SetGroupVersionKindStub
+	fake.recordInvocation("SetGroupVersionKind", []interface{}{arg1})
+	fake.setGroupVersionKindMutex.Unlock()
+	if stub != nil {
+		fake.SetGroupVersionKindStub(arg1)
+	}
+}
+
+func (fake *FakeObjectKind) SetGroupVersionKindCallCount() int {
+	fake.setGroupVersionKindMutex.RLock()
+	defer fake.setGroupVersionKindMutex.RUnlock()
+	return len(fake.setGroupVersionKindArgsForCall)
+}
+
+func (fake *FakeObjectKind) SetGroupVersionKindCalls(stub func(schema.GroupVersionKind)) {
+	fake.setGroupVersionKindMutex.Lock()
+	defer fake.setGroupVersionKindMutex.Unlock()
+	fake.SetGroupVersionKindStub = stub
+}
+
+func (fake *FakeObjectKind) SetGroupVersionKindArgsForCall(i int) schema.GroupVersionKind {
+	fake.setGroupVersionKindMutex.RLock()
+	defer fake.setGroupVersionKindMutex.RUnlock()
+	argsForCall := fake.setGroupVersionKindArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeObjectKind) Invocations() map[string][][]interface{} {
+	fake.invocationsMutex.RLock()
+	defer fake.invocationsMutex.RUnlock()
+	fake.groupVersionKindMutex.RLock()
+	defer fake.groupVersionKindMutex.RUnlock()
+	fake.setGroupVersionKindMutex.RLock()
+	defer fake.setGroupVersionKindMutex.RUnlock()
+	copiedInvocations := map[string][][]interface{}{}
+	for key, value := range fake.invocations {
+		copiedInvocations[key] = value
+	}
+	return copiedInvocations
+}
+
+func (fake *FakeObjectKind) recordInvocation(key string, args []interface{}) {
+	fake.invocationsMutex.Lock()
+	defer fake.invocationsMutex.Unlock()
+	if fake.invocations == nil {
+		fake.invocations = map[string][][]interface{}{}
+	}
+	if fake.invocations[key] == nil {
+		fake.invocations[key] = [][]interface{}{}
+	}
+	fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ schema.ObjectKind = new(FakeObjectKind)
diff --git a/internal/mode/static/policies/policiesfakes/fake_policy.go b/internal/mode/static/policies/policiesfakes/fake_policy.go
new file mode 100644
index 00000000..649d7896
--- /dev/null
+++ b/internal/mode/static/policies/policiesfakes/fake_policy.go
@@ -0,0 +1,1916 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package policiesfakes
+
+import (
+	"sync"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+)
+
+type FakePolicy struct {
+	DeepCopyObjectStub        func() runtime.Object
+	deepCopyObjectMutex       sync.RWMutex
+	deepCopyObjectArgsForCall []struct {
+	}
+	deepCopyObjectReturns struct {
+		result1 runtime.Object
+	}
+	deepCopyObjectReturnsOnCall map[int]struct {
+		result1 runtime.Object
+	}
+	GetAnnotationsStub        func() map[string]string
+	getAnnotationsMutex       sync.RWMutex
+	getAnnotationsArgsForCall []struct {
+	}
+	getAnnotationsReturns struct {
+		result1 map[string]string
+	}
+	getAnnotationsReturnsOnCall map[int]struct {
+		result1 map[string]string
+	}
+	GetCreationTimestampStub        func() v1.Time
+	getCreationTimestampMutex       sync.RWMutex
+	getCreationTimestampArgsForCall []struct {
+	}
+	getCreationTimestampReturns struct {
+		result1 v1.Time
+	}
+	getCreationTimestampReturnsOnCall map[int]struct {
+		result1 v1.Time
+	}
+	GetDeletionGracePeriodSecondsStub        func() *int64
+	getDeletionGracePeriodSecondsMutex       sync.RWMutex
+	getDeletionGracePeriodSecondsArgsForCall []struct {
+	}
+	getDeletionGracePeriodSecondsReturns struct {
+		result1 *int64
+	}
+	getDeletionGracePeriodSecondsReturnsOnCall map[int]struct {
+		result1 *int64
+	}
+	GetDeletionTimestampStub        func() *v1.Time
+	getDeletionTimestampMutex       sync.RWMutex
+	getDeletionTimestampArgsForCall []struct {
+	}
+	getDeletionTimestampReturns struct {
+		result1 *v1.Time
+	}
+	getDeletionTimestampReturnsOnCall map[int]struct {
+		result1 *v1.Time
+	}
+	GetFinalizersStub        func() []string
+	getFinalizersMutex       sync.RWMutex
+	getFinalizersArgsForCall []struct {
+	}
+	getFinalizersReturns struct {
+		result1 []string
+	}
+	getFinalizersReturnsOnCall map[int]struct {
+		result1 []string
+	}
+	GetGenerateNameStub        func() string
+	getGenerateNameMutex       sync.RWMutex
+	getGenerateNameArgsForCall []struct {
+	}
+	getGenerateNameReturns struct {
+		result1 string
+	}
+	getGenerateNameReturnsOnCall map[int]struct {
+		result1 string
+	}
+	GetGenerationStub        func() int64
+	getGenerationMutex       sync.RWMutex
+	getGenerationArgsForCall []struct {
+	}
+	getGenerationReturns struct {
+		result1 int64
+	}
+	getGenerationReturnsOnCall map[int]struct {
+		result1 int64
+	}
+	GetLabelsStub        func() map[string]string
+	getLabelsMutex       sync.RWMutex
+	getLabelsArgsForCall []struct {
+	}
+	getLabelsReturns struct {
+		result1 map[string]string
+	}
+	getLabelsReturnsOnCall map[int]struct {
+		result1 map[string]string
+	}
+	GetManagedFieldsStub        func() []v1.ManagedFieldsEntry
+	getManagedFieldsMutex       sync.RWMutex
+	getManagedFieldsArgsForCall []struct {
+	}
+	getManagedFieldsReturns struct {
+		result1 []v1.ManagedFieldsEntry
+	}
+	getManagedFieldsReturnsOnCall map[int]struct {
+		result1 []v1.ManagedFieldsEntry
+	}
+	GetNameStub        func() string
+	getNameMutex       sync.RWMutex
+	getNameArgsForCall []struct {
+	}
+	getNameReturns struct {
+		result1 string
+	}
+	getNameReturnsOnCall map[int]struct {
+		result1 string
+	}
+	GetNamespaceStub        func() string
+	getNamespaceMutex       sync.RWMutex
+	getNamespaceArgsForCall []struct {
+	}
+	getNamespaceReturns struct {
+		result1 string
+	}
+	getNamespaceReturnsOnCall map[int]struct {
+		result1 string
+	}
+	GetObjectKindStub        func() schema.ObjectKind
+	getObjectKindMutex       sync.RWMutex
+	getObjectKindArgsForCall []struct {
+	}
+	getObjectKindReturns struct {
+		result1 schema.ObjectKind
+	}
+	getObjectKindReturnsOnCall map[int]struct {
+		result1 schema.ObjectKind
+	}
+	GetOwnerReferencesStub        func() []v1.OwnerReference
+	getOwnerReferencesMutex       sync.RWMutex
+	getOwnerReferencesArgsForCall []struct {
+	}
+	getOwnerReferencesReturns struct {
+		result1 []v1.OwnerReference
+	}
+	getOwnerReferencesReturnsOnCall map[int]struct {
+		result1 []v1.OwnerReference
+	}
+	GetPolicyStatusStub        func() v1alpha2.PolicyStatus
+	getPolicyStatusMutex       sync.RWMutex
+	getPolicyStatusArgsForCall []struct {
+	}
+	getPolicyStatusReturns struct {
+		result1 v1alpha2.PolicyStatus
+	}
+	getPolicyStatusReturnsOnCall map[int]struct {
+		result1 v1alpha2.PolicyStatus
+	}
+	GetResourceVersionStub        func() string
+	getResourceVersionMutex       sync.RWMutex
+	getResourceVersionArgsForCall []struct {
+	}
+	getResourceVersionReturns struct {
+		result1 string
+	}
+	getResourceVersionReturnsOnCall map[int]struct {
+		result1 string
+	}
+	GetSelfLinkStub        func() string
+	getSelfLinkMutex       sync.RWMutex
+	getSelfLinkArgsForCall []struct {
+	}
+	getSelfLinkReturns struct {
+		result1 string
+	}
+	getSelfLinkReturnsOnCall map[int]struct {
+		result1 string
+	}
+	GetTargetRefStub        func() v1alpha2.LocalPolicyTargetReference
+	getTargetRefMutex       sync.RWMutex
+	getTargetRefArgsForCall []struct {
+	}
+	getTargetRefReturns struct {
+		result1 v1alpha2.LocalPolicyTargetReference
+	}
+	getTargetRefReturnsOnCall map[int]struct {
+		result1 v1alpha2.LocalPolicyTargetReference
+	}
+	GetUIDStub        func() types.UID
+	getUIDMutex       sync.RWMutex
+	getUIDArgsForCall []struct {
+	}
+	getUIDReturns struct {
+		result1 types.UID
+	}
+	getUIDReturnsOnCall map[int]struct {
+		result1 types.UID
+	}
+	SetAnnotationsStub        func(map[string]string)
+	setAnnotationsMutex       sync.RWMutex
+	setAnnotationsArgsForCall []struct {
+		arg1 map[string]string
+	}
+	SetCreationTimestampStub        func(v1.Time)
+	setCreationTimestampMutex       sync.RWMutex
+	setCreationTimestampArgsForCall []struct {
+		arg1 v1.Time
+	}
+	SetDeletionGracePeriodSecondsStub        func(*int64)
+	setDeletionGracePeriodSecondsMutex       sync.RWMutex
+	setDeletionGracePeriodSecondsArgsForCall []struct {
+		arg1 *int64
+	}
+	SetDeletionTimestampStub        func(*v1.Time)
+	setDeletionTimestampMutex       sync.RWMutex
+	setDeletionTimestampArgsForCall []struct {
+		arg1 *v1.Time
+	}
+	SetFinalizersStub        func([]string)
+	setFinalizersMutex       sync.RWMutex
+	setFinalizersArgsForCall []struct {
+		arg1 []string
+	}
+	SetGenerateNameStub        func(string)
+	setGenerateNameMutex       sync.RWMutex
+	setGenerateNameArgsForCall []struct {
+		arg1 string
+	}
+	SetGenerationStub        func(int64)
+	setGenerationMutex       sync.RWMutex
+	setGenerationArgsForCall []struct {
+		arg1 int64
+	}
+	SetLabelsStub        func(map[string]string)
+	setLabelsMutex       sync.RWMutex
+	setLabelsArgsForCall []struct {
+		arg1 map[string]string
+	}
+	SetManagedFieldsStub        func([]v1.ManagedFieldsEntry)
+	setManagedFieldsMutex       sync.RWMutex
+	setManagedFieldsArgsForCall []struct {
+		arg1 []v1.ManagedFieldsEntry
+	}
+	SetNameStub        func(string)
+	setNameMutex       sync.RWMutex
+	setNameArgsForCall []struct {
+		arg1 string
+	}
+	SetNamespaceStub        func(string)
+	setNamespaceMutex       sync.RWMutex
+	setNamespaceArgsForCall []struct {
+		arg1 string
+	}
+	SetOwnerReferencesStub        func([]v1.OwnerReference)
+	setOwnerReferencesMutex       sync.RWMutex
+	setOwnerReferencesArgsForCall []struct {
+		arg1 []v1.OwnerReference
+	}
+	SetPolicyStatusStub        func(v1alpha2.PolicyStatus)
+	setPolicyStatusMutex       sync.RWMutex
+	setPolicyStatusArgsForCall []struct {
+		arg1 v1alpha2.PolicyStatus
+	}
+	SetResourceVersionStub        func(string)
+	setResourceVersionMutex       sync.RWMutex
+	setResourceVersionArgsForCall []struct {
+		arg1 string
+	}
+	SetSelfLinkStub        func(string)
+	setSelfLinkMutex       sync.RWMutex
+	setSelfLinkArgsForCall []struct {
+		arg1 string
+	}
+	SetUIDStub        func(types.UID)
+	setUIDMutex       sync.RWMutex
+	setUIDArgsForCall []struct {
+		arg1 types.UID
+	}
+	invocations      map[string][][]interface{}
+	invocationsMutex sync.RWMutex
+}
+
+func (fake *FakePolicy) DeepCopyObject() runtime.Object {
+	fake.deepCopyObjectMutex.Lock()
+	ret, specificReturn := fake.deepCopyObjectReturnsOnCall[len(fake.deepCopyObjectArgsForCall)]
+	fake.deepCopyObjectArgsForCall = append(fake.deepCopyObjectArgsForCall, struct {
+	}{})
+	stub := fake.DeepCopyObjectStub
+	fakeReturns := fake.deepCopyObjectReturns
+	fake.recordInvocation("DeepCopyObject", []interface{}{})
+	fake.deepCopyObjectMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) DeepCopyObjectCallCount() int {
+	fake.deepCopyObjectMutex.RLock()
+	defer fake.deepCopyObjectMutex.RUnlock()
+	return len(fake.deepCopyObjectArgsForCall)
+}
+
+func (fake *FakePolicy) DeepCopyObjectCalls(stub func() runtime.Object) {
+	fake.deepCopyObjectMutex.Lock()
+	defer fake.deepCopyObjectMutex.Unlock()
+	fake.DeepCopyObjectStub = stub
+}
+
+func (fake *FakePolicy) DeepCopyObjectReturns(result1 runtime.Object) {
+	fake.deepCopyObjectMutex.Lock()
+	defer fake.deepCopyObjectMutex.Unlock()
+	fake.DeepCopyObjectStub = nil
+	fake.deepCopyObjectReturns = struct {
+		result1 runtime.Object
+	}{result1}
+}
+
+func (fake *FakePolicy) DeepCopyObjectReturnsOnCall(i int, result1 runtime.Object) {
+	fake.deepCopyObjectMutex.Lock()
+	defer fake.deepCopyObjectMutex.Unlock()
+	fake.DeepCopyObjectStub = nil
+	if fake.deepCopyObjectReturnsOnCall == nil {
+		fake.deepCopyObjectReturnsOnCall = make(map[int]struct {
+			result1 runtime.Object
+		})
+	}
+	fake.deepCopyObjectReturnsOnCall[i] = struct {
+		result1 runtime.Object
+	}{result1}
+}
+
+func (fake *FakePolicy) GetAnnotations() map[string]string {
+	fake.getAnnotationsMutex.Lock()
+	ret, specificReturn := fake.getAnnotationsReturnsOnCall[len(fake.getAnnotationsArgsForCall)]
+	fake.getAnnotationsArgsForCall = append(fake.getAnnotationsArgsForCall, struct {
+	}{})
+	stub := fake.GetAnnotationsStub
+	fakeReturns := fake.getAnnotationsReturns
+	fake.recordInvocation("GetAnnotations", []interface{}{})
+	fake.getAnnotationsMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetAnnotationsCallCount() int {
+	fake.getAnnotationsMutex.RLock()
+	defer fake.getAnnotationsMutex.RUnlock()
+	return len(fake.getAnnotationsArgsForCall)
+}
+
+func (fake *FakePolicy) GetAnnotationsCalls(stub func() map[string]string) {
+	fake.getAnnotationsMutex.Lock()
+	defer fake.getAnnotationsMutex.Unlock()
+	fake.GetAnnotationsStub = stub
+}
+
+func (fake *FakePolicy) GetAnnotationsReturns(result1 map[string]string) {
+	fake.getAnnotationsMutex.Lock()
+	defer fake.getAnnotationsMutex.Unlock()
+	fake.GetAnnotationsStub = nil
+	fake.getAnnotationsReturns = struct {
+		result1 map[string]string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetAnnotationsReturnsOnCall(i int, result1 map[string]string) {
+	fake.getAnnotationsMutex.Lock()
+	defer fake.getAnnotationsMutex.Unlock()
+	fake.GetAnnotationsStub = nil
+	if fake.getAnnotationsReturnsOnCall == nil {
+		fake.getAnnotationsReturnsOnCall = make(map[int]struct {
+			result1 map[string]string
+		})
+	}
+	fake.getAnnotationsReturnsOnCall[i] = struct {
+		result1 map[string]string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetCreationTimestamp() v1.Time {
+	fake.getCreationTimestampMutex.Lock()
+	ret, specificReturn := fake.getCreationTimestampReturnsOnCall[len(fake.getCreationTimestampArgsForCall)]
+	fake.getCreationTimestampArgsForCall = append(fake.getCreationTimestampArgsForCall, struct {
+	}{})
+	stub := fake.GetCreationTimestampStub
+	fakeReturns := fake.getCreationTimestampReturns
+	fake.recordInvocation("GetCreationTimestamp", []interface{}{})
+	fake.getCreationTimestampMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetCreationTimestampCallCount() int {
+	fake.getCreationTimestampMutex.RLock()
+	defer fake.getCreationTimestampMutex.RUnlock()
+	return len(fake.getCreationTimestampArgsForCall)
+}
+
+func (fake *FakePolicy) GetCreationTimestampCalls(stub func() v1.Time) {
+	fake.getCreationTimestampMutex.Lock()
+	defer fake.getCreationTimestampMutex.Unlock()
+	fake.GetCreationTimestampStub = stub
+}
+
+func (fake *FakePolicy) GetCreationTimestampReturns(result1 v1.Time) {
+	fake.getCreationTimestampMutex.Lock()
+	defer fake.getCreationTimestampMutex.Unlock()
+	fake.GetCreationTimestampStub = nil
+	fake.getCreationTimestampReturns = struct {
+		result1 v1.Time
+	}{result1}
+}
+
+func (fake *FakePolicy) GetCreationTimestampReturnsOnCall(i int, result1 v1.Time) {
+	fake.getCreationTimestampMutex.Lock()
+	defer fake.getCreationTimestampMutex.Unlock()
+	fake.GetCreationTimestampStub = nil
+	if fake.getCreationTimestampReturnsOnCall == nil {
+		fake.getCreationTimestampReturnsOnCall = make(map[int]struct {
+			result1 v1.Time
+		})
+	}
+	fake.getCreationTimestampReturnsOnCall[i] = struct {
+		result1 v1.Time
+	}{result1}
+}
+
+func (fake *FakePolicy) GetDeletionGracePeriodSeconds() *int64 {
+	fake.getDeletionGracePeriodSecondsMutex.Lock()
+	ret, specificReturn := fake.getDeletionGracePeriodSecondsReturnsOnCall[len(fake.getDeletionGracePeriodSecondsArgsForCall)]
+	fake.getDeletionGracePeriodSecondsArgsForCall = append(fake.getDeletionGracePeriodSecondsArgsForCall, struct {
+	}{})
+	stub := fake.GetDeletionGracePeriodSecondsStub
+	fakeReturns := fake.getDeletionGracePeriodSecondsReturns
+	fake.recordInvocation("GetDeletionGracePeriodSeconds", []interface{}{})
+	fake.getDeletionGracePeriodSecondsMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetDeletionGracePeriodSecondsCallCount() int {
+	fake.getDeletionGracePeriodSecondsMutex.RLock()
+	defer fake.getDeletionGracePeriodSecondsMutex.RUnlock()
+	return len(fake.getDeletionGracePeriodSecondsArgsForCall)
+}
+
+func (fake *FakePolicy) GetDeletionGracePeriodSecondsCalls(stub func() *int64) {
+	fake.getDeletionGracePeriodSecondsMutex.Lock()
+	defer fake.getDeletionGracePeriodSecondsMutex.Unlock()
+	fake.GetDeletionGracePeriodSecondsStub = stub
+}
+
+func (fake *FakePolicy) GetDeletionGracePeriodSecondsReturns(result1 *int64) {
+	fake.getDeletionGracePeriodSecondsMutex.Lock()
+	defer fake.getDeletionGracePeriodSecondsMutex.Unlock()
+	fake.GetDeletionGracePeriodSecondsStub = nil
+	fake.getDeletionGracePeriodSecondsReturns = struct {
+		result1 *int64
+	}{result1}
+}
+
+func (fake *FakePolicy) GetDeletionGracePeriodSecondsReturnsOnCall(i int, result1 *int64) {
+	fake.getDeletionGracePeriodSecondsMutex.Lock()
+	defer fake.getDeletionGracePeriodSecondsMutex.Unlock()
+	fake.GetDeletionGracePeriodSecondsStub = nil
+	if fake.getDeletionGracePeriodSecondsReturnsOnCall == nil {
+		fake.getDeletionGracePeriodSecondsReturnsOnCall = make(map[int]struct {
+			result1 *int64
+		})
+	}
+	fake.getDeletionGracePeriodSecondsReturnsOnCall[i] = struct {
+		result1 *int64
+	}{result1}
+}
+
+func (fake *FakePolicy) GetDeletionTimestamp() *v1.Time {
+	fake.getDeletionTimestampMutex.Lock()
+	ret, specificReturn := fake.getDeletionTimestampReturnsOnCall[len(fake.getDeletionTimestampArgsForCall)]
+	fake.getDeletionTimestampArgsForCall = append(fake.getDeletionTimestampArgsForCall, struct {
+	}{})
+	stub := fake.GetDeletionTimestampStub
+	fakeReturns := fake.getDeletionTimestampReturns
+	fake.recordInvocation("GetDeletionTimestamp", []interface{}{})
+	fake.getDeletionTimestampMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetDeletionTimestampCallCount() int {
+	fake.getDeletionTimestampMutex.RLock()
+	defer fake.getDeletionTimestampMutex.RUnlock()
+	return len(fake.getDeletionTimestampArgsForCall)
+}
+
+func (fake *FakePolicy) GetDeletionTimestampCalls(stub func() *v1.Time) {
+	fake.getDeletionTimestampMutex.Lock()
+	defer fake.getDeletionTimestampMutex.Unlock()
+	fake.GetDeletionTimestampStub = stub
+}
+
+func (fake *FakePolicy) GetDeletionTimestampReturns(result1 *v1.Time) {
+	fake.getDeletionTimestampMutex.Lock()
+	defer fake.getDeletionTimestampMutex.Unlock()
+	fake.GetDeletionTimestampStub = nil
+	fake.getDeletionTimestampReturns = struct {
+		result1 *v1.Time
+	}{result1}
+}
+
+func (fake *FakePolicy) GetDeletionTimestampReturnsOnCall(i int, result1 *v1.Time) {
+	fake.getDeletionTimestampMutex.Lock()
+	defer fake.getDeletionTimestampMutex.Unlock()
+	fake.GetDeletionTimestampStub = nil
+	if fake.getDeletionTimestampReturnsOnCall == nil {
+		fake.getDeletionTimestampReturnsOnCall = make(map[int]struct {
+			result1 *v1.Time
+		})
+	}
+	fake.getDeletionTimestampReturnsOnCall[i] = struct {
+		result1 *v1.Time
+	}{result1}
+}
+
+func (fake *FakePolicy) GetFinalizers() []string {
+	fake.getFinalizersMutex.Lock()
+	ret, specificReturn := fake.getFinalizersReturnsOnCall[len(fake.getFinalizersArgsForCall)]
+	fake.getFinalizersArgsForCall = append(fake.getFinalizersArgsForCall, struct {
+	}{})
+	stub := fake.GetFinalizersStub
+	fakeReturns := fake.getFinalizersReturns
+	fake.recordInvocation("GetFinalizers", []interface{}{})
+	fake.getFinalizersMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetFinalizersCallCount() int {
+	fake.getFinalizersMutex.RLock()
+	defer fake.getFinalizersMutex.RUnlock()
+	return len(fake.getFinalizersArgsForCall)
+}
+
+func (fake *FakePolicy) GetFinalizersCalls(stub func() []string) {
+	fake.getFinalizersMutex.Lock()
+	defer fake.getFinalizersMutex.Unlock()
+	fake.GetFinalizersStub = stub
+}
+
+func (fake *FakePolicy) GetFinalizersReturns(result1 []string) {
+	fake.getFinalizersMutex.Lock()
+	defer fake.getFinalizersMutex.Unlock()
+	fake.GetFinalizersStub = nil
+	fake.getFinalizersReturns = struct {
+		result1 []string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetFinalizersReturnsOnCall(i int, result1 []string) {
+	fake.getFinalizersMutex.Lock()
+	defer fake.getFinalizersMutex.Unlock()
+	fake.GetFinalizersStub = nil
+	if fake.getFinalizersReturnsOnCall == nil {
+		fake.getFinalizersReturnsOnCall = make(map[int]struct {
+			result1 []string
+		})
+	}
+	fake.getFinalizersReturnsOnCall[i] = struct {
+		result1 []string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetGenerateName() string {
+	fake.getGenerateNameMutex.Lock()
+	ret, specificReturn := fake.getGenerateNameReturnsOnCall[len(fake.getGenerateNameArgsForCall)]
+	fake.getGenerateNameArgsForCall = append(fake.getGenerateNameArgsForCall, struct {
+	}{})
+	stub := fake.GetGenerateNameStub
+	fakeReturns := fake.getGenerateNameReturns
+	fake.recordInvocation("GetGenerateName", []interface{}{})
+	fake.getGenerateNameMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetGenerateNameCallCount() int {
+	fake.getGenerateNameMutex.RLock()
+	defer fake.getGenerateNameMutex.RUnlock()
+	return len(fake.getGenerateNameArgsForCall)
+}
+
+func (fake *FakePolicy) GetGenerateNameCalls(stub func() string) {
+	fake.getGenerateNameMutex.Lock()
+	defer fake.getGenerateNameMutex.Unlock()
+	fake.GetGenerateNameStub = stub
+}
+
+func (fake *FakePolicy) GetGenerateNameReturns(result1 string) {
+	fake.getGenerateNameMutex.Lock()
+	defer fake.getGenerateNameMutex.Unlock()
+	fake.GetGenerateNameStub = nil
+	fake.getGenerateNameReturns = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetGenerateNameReturnsOnCall(i int, result1 string) {
+	fake.getGenerateNameMutex.Lock()
+	defer fake.getGenerateNameMutex.Unlock()
+	fake.GetGenerateNameStub = nil
+	if fake.getGenerateNameReturnsOnCall == nil {
+		fake.getGenerateNameReturnsOnCall = make(map[int]struct {
+			result1 string
+		})
+	}
+	fake.getGenerateNameReturnsOnCall[i] = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetGeneration() int64 {
+	fake.getGenerationMutex.Lock()
+	ret, specificReturn := fake.getGenerationReturnsOnCall[len(fake.getGenerationArgsForCall)]
+	fake.getGenerationArgsForCall = append(fake.getGenerationArgsForCall, struct {
+	}{})
+	stub := fake.GetGenerationStub
+	fakeReturns := fake.getGenerationReturns
+	fake.recordInvocation("GetGeneration", []interface{}{})
+	fake.getGenerationMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetGenerationCallCount() int {
+	fake.getGenerationMutex.RLock()
+	defer fake.getGenerationMutex.RUnlock()
+	return len(fake.getGenerationArgsForCall)
+}
+
+func (fake *FakePolicy) GetGenerationCalls(stub func() int64) {
+	fake.getGenerationMutex.Lock()
+	defer fake.getGenerationMutex.Unlock()
+	fake.GetGenerationStub = stub
+}
+
+func (fake *FakePolicy) GetGenerationReturns(result1 int64) {
+	fake.getGenerationMutex.Lock()
+	defer fake.getGenerationMutex.Unlock()
+	fake.GetGenerationStub = nil
+	fake.getGenerationReturns = struct {
+		result1 int64
+	}{result1}
+}
+
+func (fake *FakePolicy) GetGenerationReturnsOnCall(i int, result1 int64) {
+	fake.getGenerationMutex.Lock()
+	defer fake.getGenerationMutex.Unlock()
+	fake.GetGenerationStub = nil
+	if fake.getGenerationReturnsOnCall == nil {
+		fake.getGenerationReturnsOnCall = make(map[int]struct {
+			result1 int64
+		})
+	}
+	fake.getGenerationReturnsOnCall[i] = struct {
+		result1 int64
+	}{result1}
+}
+
+func (fake *FakePolicy) GetLabels() map[string]string {
+	fake.getLabelsMutex.Lock()
+	ret, specificReturn := fake.getLabelsReturnsOnCall[len(fake.getLabelsArgsForCall)]
+	fake.getLabelsArgsForCall = append(fake.getLabelsArgsForCall, struct {
+	}{})
+	stub := fake.GetLabelsStub
+	fakeReturns := fake.getLabelsReturns
+	fake.recordInvocation("GetLabels", []interface{}{})
+	fake.getLabelsMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetLabelsCallCount() int {
+	fake.getLabelsMutex.RLock()
+	defer fake.getLabelsMutex.RUnlock()
+	return len(fake.getLabelsArgsForCall)
+}
+
+func (fake *FakePolicy) GetLabelsCalls(stub func() map[string]string) {
+	fake.getLabelsMutex.Lock()
+	defer fake.getLabelsMutex.Unlock()
+	fake.GetLabelsStub = stub
+}
+
+func (fake *FakePolicy) GetLabelsReturns(result1 map[string]string) {
+	fake.getLabelsMutex.Lock()
+	defer fake.getLabelsMutex.Unlock()
+	fake.GetLabelsStub = nil
+	fake.getLabelsReturns = struct {
+		result1 map[string]string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetLabelsReturnsOnCall(i int, result1 map[string]string) {
+	fake.getLabelsMutex.Lock()
+	defer fake.getLabelsMutex.Unlock()
+	fake.GetLabelsStub = nil
+	if fake.getLabelsReturnsOnCall == nil {
+		fake.getLabelsReturnsOnCall = make(map[int]struct {
+			result1 map[string]string
+		})
+	}
+	fake.getLabelsReturnsOnCall[i] = struct {
+		result1 map[string]string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetManagedFields() []v1.ManagedFieldsEntry {
+	fake.getManagedFieldsMutex.Lock()
+	ret, specificReturn := fake.getManagedFieldsReturnsOnCall[len(fake.getManagedFieldsArgsForCall)]
+	fake.getManagedFieldsArgsForCall = append(fake.getManagedFieldsArgsForCall, struct {
+	}{})
+	stub := fake.GetManagedFieldsStub
+	fakeReturns := fake.getManagedFieldsReturns
+	fake.recordInvocation("GetManagedFields", []interface{}{})
+	fake.getManagedFieldsMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetManagedFieldsCallCount() int {
+	fake.getManagedFieldsMutex.RLock()
+	defer fake.getManagedFieldsMutex.RUnlock()
+	return len(fake.getManagedFieldsArgsForCall)
+}
+
+func (fake *FakePolicy) GetManagedFieldsCalls(stub func() []v1.ManagedFieldsEntry) {
+	fake.getManagedFieldsMutex.Lock()
+	defer fake.getManagedFieldsMutex.Unlock()
+	fake.GetManagedFieldsStub = stub
+}
+
+func (fake *FakePolicy) GetManagedFieldsReturns(result1 []v1.ManagedFieldsEntry) {
+	fake.getManagedFieldsMutex.Lock()
+	defer fake.getManagedFieldsMutex.Unlock()
+	fake.GetManagedFieldsStub = nil
+	fake.getManagedFieldsReturns = struct {
+		result1 []v1.ManagedFieldsEntry
+	}{result1}
+}
+
+func (fake *FakePolicy) GetManagedFieldsReturnsOnCall(i int, result1 []v1.ManagedFieldsEntry) {
+	fake.getManagedFieldsMutex.Lock()
+	defer fake.getManagedFieldsMutex.Unlock()
+	fake.GetManagedFieldsStub = nil
+	if fake.getManagedFieldsReturnsOnCall == nil {
+		fake.getManagedFieldsReturnsOnCall = make(map[int]struct {
+			result1 []v1.ManagedFieldsEntry
+		})
+	}
+	fake.getManagedFieldsReturnsOnCall[i] = struct {
+		result1 []v1.ManagedFieldsEntry
+	}{result1}
+}
+
+func (fake *FakePolicy) GetName() string {
+	fake.getNameMutex.Lock()
+	ret, specificReturn := fake.getNameReturnsOnCall[len(fake.getNameArgsForCall)]
+	fake.getNameArgsForCall = append(fake.getNameArgsForCall, struct {
+	}{})
+	stub := fake.GetNameStub
+	fakeReturns := fake.getNameReturns
+	fake.recordInvocation("GetName", []interface{}{})
+	fake.getNameMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetNameCallCount() int {
+	fake.getNameMutex.RLock()
+	defer fake.getNameMutex.RUnlock()
+	return len(fake.getNameArgsForCall)
+}
+
+func (fake *FakePolicy) GetNameCalls(stub func() string) {
+	fake.getNameMutex.Lock()
+	defer fake.getNameMutex.Unlock()
+	fake.GetNameStub = stub
+}
+
+func (fake *FakePolicy) GetNameReturns(result1 string) {
+	fake.getNameMutex.Lock()
+	defer fake.getNameMutex.Unlock()
+	fake.GetNameStub = nil
+	fake.getNameReturns = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetNameReturnsOnCall(i int, result1 string) {
+	fake.getNameMutex.Lock()
+	defer fake.getNameMutex.Unlock()
+	fake.GetNameStub = nil
+	if fake.getNameReturnsOnCall == nil {
+		fake.getNameReturnsOnCall = make(map[int]struct {
+			result1 string
+		})
+	}
+	fake.getNameReturnsOnCall[i] = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetNamespace() string {
+	fake.getNamespaceMutex.Lock()
+	ret, specificReturn := fake.getNamespaceReturnsOnCall[len(fake.getNamespaceArgsForCall)]
+	fake.getNamespaceArgsForCall = append(fake.getNamespaceArgsForCall, struct {
+	}{})
+	stub := fake.GetNamespaceStub
+	fakeReturns := fake.getNamespaceReturns
+	fake.recordInvocation("GetNamespace", []interface{}{})
+	fake.getNamespaceMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetNamespaceCallCount() int {
+	fake.getNamespaceMutex.RLock()
+	defer fake.getNamespaceMutex.RUnlock()
+	return len(fake.getNamespaceArgsForCall)
+}
+
+func (fake *FakePolicy) GetNamespaceCalls(stub func() string) {
+	fake.getNamespaceMutex.Lock()
+	defer fake.getNamespaceMutex.Unlock()
+	fake.GetNamespaceStub = stub
+}
+
+func (fake *FakePolicy) GetNamespaceReturns(result1 string) {
+	fake.getNamespaceMutex.Lock()
+	defer fake.getNamespaceMutex.Unlock()
+	fake.GetNamespaceStub = nil
+	fake.getNamespaceReturns = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetNamespaceReturnsOnCall(i int, result1 string) {
+	fake.getNamespaceMutex.Lock()
+	defer fake.getNamespaceMutex.Unlock()
+	fake.GetNamespaceStub = nil
+	if fake.getNamespaceReturnsOnCall == nil {
+		fake.getNamespaceReturnsOnCall = make(map[int]struct {
+			result1 string
+		})
+	}
+	fake.getNamespaceReturnsOnCall[i] = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetObjectKind() schema.ObjectKind {
+	fake.getObjectKindMutex.Lock()
+	ret, specificReturn := fake.getObjectKindReturnsOnCall[len(fake.getObjectKindArgsForCall)]
+	fake.getObjectKindArgsForCall = append(fake.getObjectKindArgsForCall, struct {
+	}{})
+	stub := fake.GetObjectKindStub
+	fakeReturns := fake.getObjectKindReturns
+	fake.recordInvocation("GetObjectKind", []interface{}{})
+	fake.getObjectKindMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetObjectKindCallCount() int {
+	fake.getObjectKindMutex.RLock()
+	defer fake.getObjectKindMutex.RUnlock()
+	return len(fake.getObjectKindArgsForCall)
+}
+
+func (fake *FakePolicy) GetObjectKindCalls(stub func() schema.ObjectKind) {
+	fake.getObjectKindMutex.Lock()
+	defer fake.getObjectKindMutex.Unlock()
+	fake.GetObjectKindStub = stub
+}
+
+func (fake *FakePolicy) GetObjectKindReturns(result1 schema.ObjectKind) {
+	fake.getObjectKindMutex.Lock()
+	defer fake.getObjectKindMutex.Unlock()
+	fake.GetObjectKindStub = nil
+	fake.getObjectKindReturns = struct {
+		result1 schema.ObjectKind
+	}{result1}
+}
+
+func (fake *FakePolicy) GetObjectKindReturnsOnCall(i int, result1 schema.ObjectKind) {
+	fake.getObjectKindMutex.Lock()
+	defer fake.getObjectKindMutex.Unlock()
+	fake.GetObjectKindStub = nil
+	if fake.getObjectKindReturnsOnCall == nil {
+		fake.getObjectKindReturnsOnCall = make(map[int]struct {
+			result1 schema.ObjectKind
+		})
+	}
+	fake.getObjectKindReturnsOnCall[i] = struct {
+		result1 schema.ObjectKind
+	}{result1}
+}
+
+func (fake *FakePolicy) GetOwnerReferences() []v1.OwnerReference {
+	fake.getOwnerReferencesMutex.Lock()
+	ret, specificReturn := fake.getOwnerReferencesReturnsOnCall[len(fake.getOwnerReferencesArgsForCall)]
+	fake.getOwnerReferencesArgsForCall = append(fake.getOwnerReferencesArgsForCall, struct {
+	}{})
+	stub := fake.GetOwnerReferencesStub
+	fakeReturns := fake.getOwnerReferencesReturns
+	fake.recordInvocation("GetOwnerReferences", []interface{}{})
+	fake.getOwnerReferencesMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetOwnerReferencesCallCount() int {
+	fake.getOwnerReferencesMutex.RLock()
+	defer fake.getOwnerReferencesMutex.RUnlock()
+	return len(fake.getOwnerReferencesArgsForCall)
+}
+
+func (fake *FakePolicy) GetOwnerReferencesCalls(stub func() []v1.OwnerReference) {
+	fake.getOwnerReferencesMutex.Lock()
+	defer fake.getOwnerReferencesMutex.Unlock()
+	fake.GetOwnerReferencesStub = stub
+}
+
+func (fake *FakePolicy) GetOwnerReferencesReturns(result1 []v1.OwnerReference) {
+	fake.getOwnerReferencesMutex.Lock()
+	defer fake.getOwnerReferencesMutex.Unlock()
+	fake.GetOwnerReferencesStub = nil
+	fake.getOwnerReferencesReturns = struct {
+		result1 []v1.OwnerReference
+	}{result1}
+}
+
+func (fake *FakePolicy) GetOwnerReferencesReturnsOnCall(i int, result1 []v1.OwnerReference) {
+	fake.getOwnerReferencesMutex.Lock()
+	defer fake.getOwnerReferencesMutex.Unlock()
+	fake.GetOwnerReferencesStub = nil
+	if fake.getOwnerReferencesReturnsOnCall == nil {
+		fake.getOwnerReferencesReturnsOnCall = make(map[int]struct {
+			result1 []v1.OwnerReference
+		})
+	}
+	fake.getOwnerReferencesReturnsOnCall[i] = struct {
+		result1 []v1.OwnerReference
+	}{result1}
+}
+
+func (fake *FakePolicy) GetPolicyStatus() v1alpha2.PolicyStatus {
+	fake.getPolicyStatusMutex.Lock()
+	ret, specificReturn := fake.getPolicyStatusReturnsOnCall[len(fake.getPolicyStatusArgsForCall)]
+	fake.getPolicyStatusArgsForCall = append(fake.getPolicyStatusArgsForCall, struct {
+	}{})
+	stub := fake.GetPolicyStatusStub
+	fakeReturns := fake.getPolicyStatusReturns
+	fake.recordInvocation("GetPolicyStatus", []interface{}{})
+	fake.getPolicyStatusMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetPolicyStatusCallCount() int {
+	fake.getPolicyStatusMutex.RLock()
+	defer fake.getPolicyStatusMutex.RUnlock()
+	return len(fake.getPolicyStatusArgsForCall)
+}
+
+func (fake *FakePolicy) GetPolicyStatusCalls(stub func() v1alpha2.PolicyStatus) {
+	fake.getPolicyStatusMutex.Lock()
+	defer fake.getPolicyStatusMutex.Unlock()
+	fake.GetPolicyStatusStub = stub
+}
+
+func (fake *FakePolicy) GetPolicyStatusReturns(result1 v1alpha2.PolicyStatus) {
+	fake.getPolicyStatusMutex.Lock()
+	defer fake.getPolicyStatusMutex.Unlock()
+	fake.GetPolicyStatusStub = nil
+	fake.getPolicyStatusReturns = struct {
+		result1 v1alpha2.PolicyStatus
+	}{result1}
+}
+
+func (fake *FakePolicy) GetPolicyStatusReturnsOnCall(i int, result1 v1alpha2.PolicyStatus) {
+	fake.getPolicyStatusMutex.Lock()
+	defer fake.getPolicyStatusMutex.Unlock()
+	fake.GetPolicyStatusStub = nil
+	if fake.getPolicyStatusReturnsOnCall == nil {
+		fake.getPolicyStatusReturnsOnCall = make(map[int]struct {
+			result1 v1alpha2.PolicyStatus
+		})
+	}
+	fake.getPolicyStatusReturnsOnCall[i] = struct {
+		result1 v1alpha2.PolicyStatus
+	}{result1}
+}
+
+func (fake *FakePolicy) GetResourceVersion() string {
+	fake.getResourceVersionMutex.Lock()
+	ret, specificReturn := fake.getResourceVersionReturnsOnCall[len(fake.getResourceVersionArgsForCall)]
+	fake.getResourceVersionArgsForCall = append(fake.getResourceVersionArgsForCall, struct {
+	}{})
+	stub := fake.GetResourceVersionStub
+	fakeReturns := fake.getResourceVersionReturns
+	fake.recordInvocation("GetResourceVersion", []interface{}{})
+	fake.getResourceVersionMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetResourceVersionCallCount() int {
+	fake.getResourceVersionMutex.RLock()
+	defer fake.getResourceVersionMutex.RUnlock()
+	return len(fake.getResourceVersionArgsForCall)
+}
+
+func (fake *FakePolicy) GetResourceVersionCalls(stub func() string) {
+	fake.getResourceVersionMutex.Lock()
+	defer fake.getResourceVersionMutex.Unlock()
+	fake.GetResourceVersionStub = stub
+}
+
+func (fake *FakePolicy) GetResourceVersionReturns(result1 string) {
+	fake.getResourceVersionMutex.Lock()
+	defer fake.getResourceVersionMutex.Unlock()
+	fake.GetResourceVersionStub = nil
+	fake.getResourceVersionReturns = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetResourceVersionReturnsOnCall(i int, result1 string) {
+	fake.getResourceVersionMutex.Lock()
+	defer fake.getResourceVersionMutex.Unlock()
+	fake.GetResourceVersionStub = nil
+	if fake.getResourceVersionReturnsOnCall == nil {
+		fake.getResourceVersionReturnsOnCall = make(map[int]struct {
+			result1 string
+		})
+	}
+	fake.getResourceVersionReturnsOnCall[i] = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetSelfLink() string {
+	fake.getSelfLinkMutex.Lock()
+	ret, specificReturn := fake.getSelfLinkReturnsOnCall[len(fake.getSelfLinkArgsForCall)]
+	fake.getSelfLinkArgsForCall = append(fake.getSelfLinkArgsForCall, struct {
+	}{})
+	stub := fake.GetSelfLinkStub
+	fakeReturns := fake.getSelfLinkReturns
+	fake.recordInvocation("GetSelfLink", []interface{}{})
+	fake.getSelfLinkMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetSelfLinkCallCount() int {
+	fake.getSelfLinkMutex.RLock()
+	defer fake.getSelfLinkMutex.RUnlock()
+	return len(fake.getSelfLinkArgsForCall)
+}
+
+func (fake *FakePolicy) GetSelfLinkCalls(stub func() string) {
+	fake.getSelfLinkMutex.Lock()
+	defer fake.getSelfLinkMutex.Unlock()
+	fake.GetSelfLinkStub = stub
+}
+
+func (fake *FakePolicy) GetSelfLinkReturns(result1 string) {
+	fake.getSelfLinkMutex.Lock()
+	defer fake.getSelfLinkMutex.Unlock()
+	fake.GetSelfLinkStub = nil
+	fake.getSelfLinkReturns = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetSelfLinkReturnsOnCall(i int, result1 string) {
+	fake.getSelfLinkMutex.Lock()
+	defer fake.getSelfLinkMutex.Unlock()
+	fake.GetSelfLinkStub = nil
+	if fake.getSelfLinkReturnsOnCall == nil {
+		fake.getSelfLinkReturnsOnCall = make(map[int]struct {
+			result1 string
+		})
+	}
+	fake.getSelfLinkReturnsOnCall[i] = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakePolicy) GetTargetRef() v1alpha2.LocalPolicyTargetReference {
+	fake.getTargetRefMutex.Lock()
+	ret, specificReturn := fake.getTargetRefReturnsOnCall[len(fake.getTargetRefArgsForCall)]
+	fake.getTargetRefArgsForCall = append(fake.getTargetRefArgsForCall, struct {
+	}{})
+	stub := fake.GetTargetRefStub
+	fakeReturns := fake.getTargetRefReturns
+	fake.recordInvocation("GetTargetRef", []interface{}{})
+	fake.getTargetRefMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetTargetRefCallCount() int {
+	fake.getTargetRefMutex.RLock()
+	defer fake.getTargetRefMutex.RUnlock()
+	return len(fake.getTargetRefArgsForCall)
+}
+
+func (fake *FakePolicy) GetTargetRefCalls(stub func() v1alpha2.LocalPolicyTargetReference) {
+	fake.getTargetRefMutex.Lock()
+	defer fake.getTargetRefMutex.Unlock()
+	fake.GetTargetRefStub = stub
+}
+
+func (fake *FakePolicy) GetTargetRefReturns(result1 v1alpha2.LocalPolicyTargetReference) {
+	fake.getTargetRefMutex.Lock()
+	defer fake.getTargetRefMutex.Unlock()
+	fake.GetTargetRefStub = nil
+	fake.getTargetRefReturns = struct {
+		result1 v1alpha2.LocalPolicyTargetReference
+	}{result1}
+}
+
+func (fake *FakePolicy) GetTargetRefReturnsOnCall(i int, result1 v1alpha2.LocalPolicyTargetReference) {
+	fake.getTargetRefMutex.Lock()
+	defer fake.getTargetRefMutex.Unlock()
+	fake.GetTargetRefStub = nil
+	if fake.getTargetRefReturnsOnCall == nil {
+		fake.getTargetRefReturnsOnCall = make(map[int]struct {
+			result1 v1alpha2.LocalPolicyTargetReference
+		})
+	}
+	fake.getTargetRefReturnsOnCall[i] = struct {
+		result1 v1alpha2.LocalPolicyTargetReference
+	}{result1}
+}
+
+func (fake *FakePolicy) GetUID() types.UID {
+	fake.getUIDMutex.Lock()
+	ret, specificReturn := fake.getUIDReturnsOnCall[len(fake.getUIDArgsForCall)]
+	fake.getUIDArgsForCall = append(fake.getUIDArgsForCall, struct {
+	}{})
+	stub := fake.GetUIDStub
+	fakeReturns := fake.getUIDReturns
+	fake.recordInvocation("GetUID", []interface{}{})
+	fake.getUIDMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicy) GetUIDCallCount() int {
+	fake.getUIDMutex.RLock()
+	defer fake.getUIDMutex.RUnlock()
+	return len(fake.getUIDArgsForCall)
+}
+
+func (fake *FakePolicy) GetUIDCalls(stub func() types.UID) {
+	fake.getUIDMutex.Lock()
+	defer fake.getUIDMutex.Unlock()
+	fake.GetUIDStub = stub
+}
+
+func (fake *FakePolicy) GetUIDReturns(result1 types.UID) {
+	fake.getUIDMutex.Lock()
+	defer fake.getUIDMutex.Unlock()
+	fake.GetUIDStub = nil
+	fake.getUIDReturns = struct {
+		result1 types.UID
+	}{result1}
+}
+
+func (fake *FakePolicy) GetUIDReturnsOnCall(i int, result1 types.UID) {
+	fake.getUIDMutex.Lock()
+	defer fake.getUIDMutex.Unlock()
+	fake.GetUIDStub = nil
+	if fake.getUIDReturnsOnCall == nil {
+		fake.getUIDReturnsOnCall = make(map[int]struct {
+			result1 types.UID
+		})
+	}
+	fake.getUIDReturnsOnCall[i] = struct {
+		result1 types.UID
+	}{result1}
+}
+
+func (fake *FakePolicy) SetAnnotations(arg1 map[string]string) {
+	fake.setAnnotationsMutex.Lock()
+	fake.setAnnotationsArgsForCall = append(fake.setAnnotationsArgsForCall, struct {
+		arg1 map[string]string
+	}{arg1})
+	stub := fake.SetAnnotationsStub
+	fake.recordInvocation("SetAnnotations", []interface{}{arg1})
+	fake.setAnnotationsMutex.Unlock()
+	if stub != nil {
+		fake.SetAnnotationsStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetAnnotationsCallCount() int {
+	fake.setAnnotationsMutex.RLock()
+	defer fake.setAnnotationsMutex.RUnlock()
+	return len(fake.setAnnotationsArgsForCall)
+}
+
+func (fake *FakePolicy) SetAnnotationsCalls(stub func(map[string]string)) {
+	fake.setAnnotationsMutex.Lock()
+	defer fake.setAnnotationsMutex.Unlock()
+	fake.SetAnnotationsStub = stub
+}
+
+func (fake *FakePolicy) SetAnnotationsArgsForCall(i int) map[string]string {
+	fake.setAnnotationsMutex.RLock()
+	defer fake.setAnnotationsMutex.RUnlock()
+	argsForCall := fake.setAnnotationsArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetCreationTimestamp(arg1 v1.Time) {
+	fake.setCreationTimestampMutex.Lock()
+	fake.setCreationTimestampArgsForCall = append(fake.setCreationTimestampArgsForCall, struct {
+		arg1 v1.Time
+	}{arg1})
+	stub := fake.SetCreationTimestampStub
+	fake.recordInvocation("SetCreationTimestamp", []interface{}{arg1})
+	fake.setCreationTimestampMutex.Unlock()
+	if stub != nil {
+		fake.SetCreationTimestampStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetCreationTimestampCallCount() int {
+	fake.setCreationTimestampMutex.RLock()
+	defer fake.setCreationTimestampMutex.RUnlock()
+	return len(fake.setCreationTimestampArgsForCall)
+}
+
+func (fake *FakePolicy) SetCreationTimestampCalls(stub func(v1.Time)) {
+	fake.setCreationTimestampMutex.Lock()
+	defer fake.setCreationTimestampMutex.Unlock()
+	fake.SetCreationTimestampStub = stub
+}
+
+func (fake *FakePolicy) SetCreationTimestampArgsForCall(i int) v1.Time {
+	fake.setCreationTimestampMutex.RLock()
+	defer fake.setCreationTimestampMutex.RUnlock()
+	argsForCall := fake.setCreationTimestampArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetDeletionGracePeriodSeconds(arg1 *int64) {
+	fake.setDeletionGracePeriodSecondsMutex.Lock()
+	fake.setDeletionGracePeriodSecondsArgsForCall = append(fake.setDeletionGracePeriodSecondsArgsForCall, struct {
+		arg1 *int64
+	}{arg1})
+	stub := fake.SetDeletionGracePeriodSecondsStub
+	fake.recordInvocation("SetDeletionGracePeriodSeconds", []interface{}{arg1})
+	fake.setDeletionGracePeriodSecondsMutex.Unlock()
+	if stub != nil {
+		fake.SetDeletionGracePeriodSecondsStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetDeletionGracePeriodSecondsCallCount() int {
+	fake.setDeletionGracePeriodSecondsMutex.RLock()
+	defer fake.setDeletionGracePeriodSecondsMutex.RUnlock()
+	return len(fake.setDeletionGracePeriodSecondsArgsForCall)
+}
+
+func (fake *FakePolicy) SetDeletionGracePeriodSecondsCalls(stub func(*int64)) {
+	fake.setDeletionGracePeriodSecondsMutex.Lock()
+	defer fake.setDeletionGracePeriodSecondsMutex.Unlock()
+	fake.SetDeletionGracePeriodSecondsStub = stub
+}
+
+func (fake *FakePolicy) SetDeletionGracePeriodSecondsArgsForCall(i int) *int64 {
+	fake.setDeletionGracePeriodSecondsMutex.RLock()
+	defer fake.setDeletionGracePeriodSecondsMutex.RUnlock()
+	argsForCall := fake.setDeletionGracePeriodSecondsArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetDeletionTimestamp(arg1 *v1.Time) {
+	fake.setDeletionTimestampMutex.Lock()
+	fake.setDeletionTimestampArgsForCall = append(fake.setDeletionTimestampArgsForCall, struct {
+		arg1 *v1.Time
+	}{arg1})
+	stub := fake.SetDeletionTimestampStub
+	fake.recordInvocation("SetDeletionTimestamp", []interface{}{arg1})
+	fake.setDeletionTimestampMutex.Unlock()
+	if stub != nil {
+		fake.SetDeletionTimestampStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetDeletionTimestampCallCount() int {
+	fake.setDeletionTimestampMutex.RLock()
+	defer fake.setDeletionTimestampMutex.RUnlock()
+	return len(fake.setDeletionTimestampArgsForCall)
+}
+
+func (fake *FakePolicy) SetDeletionTimestampCalls(stub func(*v1.Time)) {
+	fake.setDeletionTimestampMutex.Lock()
+	defer fake.setDeletionTimestampMutex.Unlock()
+	fake.SetDeletionTimestampStub = stub
+}
+
+func (fake *FakePolicy) SetDeletionTimestampArgsForCall(i int) *v1.Time {
+	fake.setDeletionTimestampMutex.RLock()
+	defer fake.setDeletionTimestampMutex.RUnlock()
+	argsForCall := fake.setDeletionTimestampArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetFinalizers(arg1 []string) {
+	var arg1Copy []string
+	if arg1 != nil {
+		arg1Copy = make([]string, len(arg1))
+		copy(arg1Copy, arg1)
+	}
+	fake.setFinalizersMutex.Lock()
+	fake.setFinalizersArgsForCall = append(fake.setFinalizersArgsForCall, struct {
+		arg1 []string
+	}{arg1Copy})
+	stub := fake.SetFinalizersStub
+	fake.recordInvocation("SetFinalizers", []interface{}{arg1Copy})
+	fake.setFinalizersMutex.Unlock()
+	if stub != nil {
+		fake.SetFinalizersStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetFinalizersCallCount() int {
+	fake.setFinalizersMutex.RLock()
+	defer fake.setFinalizersMutex.RUnlock()
+	return len(fake.setFinalizersArgsForCall)
+}
+
+func (fake *FakePolicy) SetFinalizersCalls(stub func([]string)) {
+	fake.setFinalizersMutex.Lock()
+	defer fake.setFinalizersMutex.Unlock()
+	fake.SetFinalizersStub = stub
+}
+
+func (fake *FakePolicy) SetFinalizersArgsForCall(i int) []string {
+	fake.setFinalizersMutex.RLock()
+	defer fake.setFinalizersMutex.RUnlock()
+	argsForCall := fake.setFinalizersArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetGenerateName(arg1 string) {
+	fake.setGenerateNameMutex.Lock()
+	fake.setGenerateNameArgsForCall = append(fake.setGenerateNameArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.SetGenerateNameStub
+	fake.recordInvocation("SetGenerateName", []interface{}{arg1})
+	fake.setGenerateNameMutex.Unlock()
+	if stub != nil {
+		fake.SetGenerateNameStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetGenerateNameCallCount() int {
+	fake.setGenerateNameMutex.RLock()
+	defer fake.setGenerateNameMutex.RUnlock()
+	return len(fake.setGenerateNameArgsForCall)
+}
+
+func (fake *FakePolicy) SetGenerateNameCalls(stub func(string)) {
+	fake.setGenerateNameMutex.Lock()
+	defer fake.setGenerateNameMutex.Unlock()
+	fake.SetGenerateNameStub = stub
+}
+
+func (fake *FakePolicy) SetGenerateNameArgsForCall(i int) string {
+	fake.setGenerateNameMutex.RLock()
+	defer fake.setGenerateNameMutex.RUnlock()
+	argsForCall := fake.setGenerateNameArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetGeneration(arg1 int64) {
+	fake.setGenerationMutex.Lock()
+	fake.setGenerationArgsForCall = append(fake.setGenerationArgsForCall, struct {
+		arg1 int64
+	}{arg1})
+	stub := fake.SetGenerationStub
+	fake.recordInvocation("SetGeneration", []interface{}{arg1})
+	fake.setGenerationMutex.Unlock()
+	if stub != nil {
+		fake.SetGenerationStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetGenerationCallCount() int {
+	fake.setGenerationMutex.RLock()
+	defer fake.setGenerationMutex.RUnlock()
+	return len(fake.setGenerationArgsForCall)
+}
+
+func (fake *FakePolicy) SetGenerationCalls(stub func(int64)) {
+	fake.setGenerationMutex.Lock()
+	defer fake.setGenerationMutex.Unlock()
+	fake.SetGenerationStub = stub
+}
+
+func (fake *FakePolicy) SetGenerationArgsForCall(i int) int64 {
+	fake.setGenerationMutex.RLock()
+	defer fake.setGenerationMutex.RUnlock()
+	argsForCall := fake.setGenerationArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetLabels(arg1 map[string]string) {
+	fake.setLabelsMutex.Lock()
+	fake.setLabelsArgsForCall = append(fake.setLabelsArgsForCall, struct {
+		arg1 map[string]string
+	}{arg1})
+	stub := fake.SetLabelsStub
+	fake.recordInvocation("SetLabels", []interface{}{arg1})
+	fake.setLabelsMutex.Unlock()
+	if stub != nil {
+		fake.SetLabelsStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetLabelsCallCount() int {
+	fake.setLabelsMutex.RLock()
+	defer fake.setLabelsMutex.RUnlock()
+	return len(fake.setLabelsArgsForCall)
+}
+
+func (fake *FakePolicy) SetLabelsCalls(stub func(map[string]string)) {
+	fake.setLabelsMutex.Lock()
+	defer fake.setLabelsMutex.Unlock()
+	fake.SetLabelsStub = stub
+}
+
+func (fake *FakePolicy) SetLabelsArgsForCall(i int) map[string]string {
+	fake.setLabelsMutex.RLock()
+	defer fake.setLabelsMutex.RUnlock()
+	argsForCall := fake.setLabelsArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetManagedFields(arg1 []v1.ManagedFieldsEntry) {
+	var arg1Copy []v1.ManagedFieldsEntry
+	if arg1 != nil {
+		arg1Copy = make([]v1.ManagedFieldsEntry, len(arg1))
+		copy(arg1Copy, arg1)
+	}
+	fake.setManagedFieldsMutex.Lock()
+	fake.setManagedFieldsArgsForCall = append(fake.setManagedFieldsArgsForCall, struct {
+		arg1 []v1.ManagedFieldsEntry
+	}{arg1Copy})
+	stub := fake.SetManagedFieldsStub
+	fake.recordInvocation("SetManagedFields", []interface{}{arg1Copy})
+	fake.setManagedFieldsMutex.Unlock()
+	if stub != nil {
+		fake.SetManagedFieldsStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetManagedFieldsCallCount() int {
+	fake.setManagedFieldsMutex.RLock()
+	defer fake.setManagedFieldsMutex.RUnlock()
+	return len(fake.setManagedFieldsArgsForCall)
+}
+
+func (fake *FakePolicy) SetManagedFieldsCalls(stub func([]v1.ManagedFieldsEntry)) {
+	fake.setManagedFieldsMutex.Lock()
+	defer fake.setManagedFieldsMutex.Unlock()
+	fake.SetManagedFieldsStub = stub
+}
+
+func (fake *FakePolicy) SetManagedFieldsArgsForCall(i int) []v1.ManagedFieldsEntry {
+	fake.setManagedFieldsMutex.RLock()
+	defer fake.setManagedFieldsMutex.RUnlock()
+	argsForCall := fake.setManagedFieldsArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetName(arg1 string) {
+	fake.setNameMutex.Lock()
+	fake.setNameArgsForCall = append(fake.setNameArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.SetNameStub
+	fake.recordInvocation("SetName", []interface{}{arg1})
+	fake.setNameMutex.Unlock()
+	if stub != nil {
+		fake.SetNameStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetNameCallCount() int {
+	fake.setNameMutex.RLock()
+	defer fake.setNameMutex.RUnlock()
+	return len(fake.setNameArgsForCall)
+}
+
+func (fake *FakePolicy) SetNameCalls(stub func(string)) {
+	fake.setNameMutex.Lock()
+	defer fake.setNameMutex.Unlock()
+	fake.SetNameStub = stub
+}
+
+func (fake *FakePolicy) SetNameArgsForCall(i int) string {
+	fake.setNameMutex.RLock()
+	defer fake.setNameMutex.RUnlock()
+	argsForCall := fake.setNameArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetNamespace(arg1 string) {
+	fake.setNamespaceMutex.Lock()
+	fake.setNamespaceArgsForCall = append(fake.setNamespaceArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.SetNamespaceStub
+	fake.recordInvocation("SetNamespace", []interface{}{arg1})
+	fake.setNamespaceMutex.Unlock()
+	if stub != nil {
+		fake.SetNamespaceStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetNamespaceCallCount() int {
+	fake.setNamespaceMutex.RLock()
+	defer fake.setNamespaceMutex.RUnlock()
+	return len(fake.setNamespaceArgsForCall)
+}
+
+func (fake *FakePolicy) SetNamespaceCalls(stub func(string)) {
+	fake.setNamespaceMutex.Lock()
+	defer fake.setNamespaceMutex.Unlock()
+	fake.SetNamespaceStub = stub
+}
+
+func (fake *FakePolicy) SetNamespaceArgsForCall(i int) string {
+	fake.setNamespaceMutex.RLock()
+	defer fake.setNamespaceMutex.RUnlock()
+	argsForCall := fake.setNamespaceArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetOwnerReferences(arg1 []v1.OwnerReference) {
+	var arg1Copy []v1.OwnerReference
+	if arg1 != nil {
+		arg1Copy = make([]v1.OwnerReference, len(arg1))
+		copy(arg1Copy, arg1)
+	}
+	fake.setOwnerReferencesMutex.Lock()
+	fake.setOwnerReferencesArgsForCall = append(fake.setOwnerReferencesArgsForCall, struct {
+		arg1 []v1.OwnerReference
+	}{arg1Copy})
+	stub := fake.SetOwnerReferencesStub
+	fake.recordInvocation("SetOwnerReferences", []interface{}{arg1Copy})
+	fake.setOwnerReferencesMutex.Unlock()
+	if stub != nil {
+		fake.SetOwnerReferencesStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetOwnerReferencesCallCount() int {
+	fake.setOwnerReferencesMutex.RLock()
+	defer fake.setOwnerReferencesMutex.RUnlock()
+	return len(fake.setOwnerReferencesArgsForCall)
+}
+
+func (fake *FakePolicy) SetOwnerReferencesCalls(stub func([]v1.OwnerReference)) {
+	fake.setOwnerReferencesMutex.Lock()
+	defer fake.setOwnerReferencesMutex.Unlock()
+	fake.SetOwnerReferencesStub = stub
+}
+
+func (fake *FakePolicy) SetOwnerReferencesArgsForCall(i int) []v1.OwnerReference {
+	fake.setOwnerReferencesMutex.RLock()
+	defer fake.setOwnerReferencesMutex.RUnlock()
+	argsForCall := fake.setOwnerReferencesArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetPolicyStatus(arg1 v1alpha2.PolicyStatus) {
+	fake.setPolicyStatusMutex.Lock()
+	fake.setPolicyStatusArgsForCall = append(fake.setPolicyStatusArgsForCall, struct {
+		arg1 v1alpha2.PolicyStatus
+	}{arg1})
+	stub := fake.SetPolicyStatusStub
+	fake.recordInvocation("SetPolicyStatus", []interface{}{arg1})
+	fake.setPolicyStatusMutex.Unlock()
+	if stub != nil {
+		fake.SetPolicyStatusStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetPolicyStatusCallCount() int {
+	fake.setPolicyStatusMutex.RLock()
+	defer fake.setPolicyStatusMutex.RUnlock()
+	return len(fake.setPolicyStatusArgsForCall)
+}
+
+func (fake *FakePolicy) SetPolicyStatusCalls(stub func(v1alpha2.PolicyStatus)) {
+	fake.setPolicyStatusMutex.Lock()
+	defer fake.setPolicyStatusMutex.Unlock()
+	fake.SetPolicyStatusStub = stub
+}
+
+func (fake *FakePolicy) SetPolicyStatusArgsForCall(i int) v1alpha2.PolicyStatus {
+	fake.setPolicyStatusMutex.RLock()
+	defer fake.setPolicyStatusMutex.RUnlock()
+	argsForCall := fake.setPolicyStatusArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetResourceVersion(arg1 string) {
+	fake.setResourceVersionMutex.Lock()
+	fake.setResourceVersionArgsForCall = append(fake.setResourceVersionArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.SetResourceVersionStub
+	fake.recordInvocation("SetResourceVersion", []interface{}{arg1})
+	fake.setResourceVersionMutex.Unlock()
+	if stub != nil {
+		fake.SetResourceVersionStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetResourceVersionCallCount() int {
+	fake.setResourceVersionMutex.RLock()
+	defer fake.setResourceVersionMutex.RUnlock()
+	return len(fake.setResourceVersionArgsForCall)
+}
+
+func (fake *FakePolicy) SetResourceVersionCalls(stub func(string)) {
+	fake.setResourceVersionMutex.Lock()
+	defer fake.setResourceVersionMutex.Unlock()
+	fake.SetResourceVersionStub = stub
+}
+
+func (fake *FakePolicy) SetResourceVersionArgsForCall(i int) string {
+	fake.setResourceVersionMutex.RLock()
+	defer fake.setResourceVersionMutex.RUnlock()
+	argsForCall := fake.setResourceVersionArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetSelfLink(arg1 string) {
+	fake.setSelfLinkMutex.Lock()
+	fake.setSelfLinkArgsForCall = append(fake.setSelfLinkArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.SetSelfLinkStub
+	fake.recordInvocation("SetSelfLink", []interface{}{arg1})
+	fake.setSelfLinkMutex.Unlock()
+	if stub != nil {
+		fake.SetSelfLinkStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetSelfLinkCallCount() int {
+	fake.setSelfLinkMutex.RLock()
+	defer fake.setSelfLinkMutex.RUnlock()
+	return len(fake.setSelfLinkArgsForCall)
+}
+
+func (fake *FakePolicy) SetSelfLinkCalls(stub func(string)) {
+	fake.setSelfLinkMutex.Lock()
+	defer fake.setSelfLinkMutex.Unlock()
+	fake.SetSelfLinkStub = stub
+}
+
+func (fake *FakePolicy) SetSelfLinkArgsForCall(i int) string {
+	fake.setSelfLinkMutex.RLock()
+	defer fake.setSelfLinkMutex.RUnlock()
+	argsForCall := fake.setSelfLinkArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) SetUID(arg1 types.UID) {
+	fake.setUIDMutex.Lock()
+	fake.setUIDArgsForCall = append(fake.setUIDArgsForCall, struct {
+		arg1 types.UID
+	}{arg1})
+	stub := fake.SetUIDStub
+	fake.recordInvocation("SetUID", []interface{}{arg1})
+	fake.setUIDMutex.Unlock()
+	if stub != nil {
+		fake.SetUIDStub(arg1)
+	}
+}
+
+func (fake *FakePolicy) SetUIDCallCount() int {
+	fake.setUIDMutex.RLock()
+	defer fake.setUIDMutex.RUnlock()
+	return len(fake.setUIDArgsForCall)
+}
+
+func (fake *FakePolicy) SetUIDCalls(stub func(types.UID)) {
+	fake.setUIDMutex.Lock()
+	defer fake.setUIDMutex.Unlock()
+	fake.SetUIDStub = stub
+}
+
+func (fake *FakePolicy) SetUIDArgsForCall(i int) types.UID {
+	fake.setUIDMutex.RLock()
+	defer fake.setUIDMutex.RUnlock()
+	argsForCall := fake.setUIDArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicy) Invocations() map[string][][]interface{} {
+	fake.invocationsMutex.RLock()
+	defer fake.invocationsMutex.RUnlock()
+	fake.deepCopyObjectMutex.RLock()
+	defer fake.deepCopyObjectMutex.RUnlock()
+	fake.getAnnotationsMutex.RLock()
+	defer fake.getAnnotationsMutex.RUnlock()
+	fake.getCreationTimestampMutex.RLock()
+	defer fake.getCreationTimestampMutex.RUnlock()
+	fake.getDeletionGracePeriodSecondsMutex.RLock()
+	defer fake.getDeletionGracePeriodSecondsMutex.RUnlock()
+	fake.getDeletionTimestampMutex.RLock()
+	defer fake.getDeletionTimestampMutex.RUnlock()
+	fake.getFinalizersMutex.RLock()
+	defer fake.getFinalizersMutex.RUnlock()
+	fake.getGenerateNameMutex.RLock()
+	defer fake.getGenerateNameMutex.RUnlock()
+	fake.getGenerationMutex.RLock()
+	defer fake.getGenerationMutex.RUnlock()
+	fake.getLabelsMutex.RLock()
+	defer fake.getLabelsMutex.RUnlock()
+	fake.getManagedFieldsMutex.RLock()
+	defer fake.getManagedFieldsMutex.RUnlock()
+	fake.getNameMutex.RLock()
+	defer fake.getNameMutex.RUnlock()
+	fake.getNamespaceMutex.RLock()
+	defer fake.getNamespaceMutex.RUnlock()
+	fake.getObjectKindMutex.RLock()
+	defer fake.getObjectKindMutex.RUnlock()
+	fake.getOwnerReferencesMutex.RLock()
+	defer fake.getOwnerReferencesMutex.RUnlock()
+	fake.getPolicyStatusMutex.RLock()
+	defer fake.getPolicyStatusMutex.RUnlock()
+	fake.getResourceVersionMutex.RLock()
+	defer fake.getResourceVersionMutex.RUnlock()
+	fake.getSelfLinkMutex.RLock()
+	defer fake.getSelfLinkMutex.RUnlock()
+	fake.getTargetRefMutex.RLock()
+	defer fake.getTargetRefMutex.RUnlock()
+	fake.getUIDMutex.RLock()
+	defer fake.getUIDMutex.RUnlock()
+	fake.setAnnotationsMutex.RLock()
+	defer fake.setAnnotationsMutex.RUnlock()
+	fake.setCreationTimestampMutex.RLock()
+	defer fake.setCreationTimestampMutex.RUnlock()
+	fake.setDeletionGracePeriodSecondsMutex.RLock()
+	defer fake.setDeletionGracePeriodSecondsMutex.RUnlock()
+	fake.setDeletionTimestampMutex.RLock()
+	defer fake.setDeletionTimestampMutex.RUnlock()
+	fake.setFinalizersMutex.RLock()
+	defer fake.setFinalizersMutex.RUnlock()
+	fake.setGenerateNameMutex.RLock()
+	defer fake.setGenerateNameMutex.RUnlock()
+	fake.setGenerationMutex.RLock()
+	defer fake.setGenerationMutex.RUnlock()
+	fake.setLabelsMutex.RLock()
+	defer fake.setLabelsMutex.RUnlock()
+	fake.setManagedFieldsMutex.RLock()
+	defer fake.setManagedFieldsMutex.RUnlock()
+	fake.setNameMutex.RLock()
+	defer fake.setNameMutex.RUnlock()
+	fake.setNamespaceMutex.RLock()
+	defer fake.setNamespaceMutex.RUnlock()
+	fake.setOwnerReferencesMutex.RLock()
+	defer fake.setOwnerReferencesMutex.RUnlock()
+	fake.setPolicyStatusMutex.RLock()
+	defer fake.setPolicyStatusMutex.RUnlock()
+	fake.setResourceVersionMutex.RLock()
+	defer fake.setResourceVersionMutex.RUnlock()
+	fake.setSelfLinkMutex.RLock()
+	defer fake.setSelfLinkMutex.RUnlock()
+	fake.setUIDMutex.RLock()
+	defer fake.setUIDMutex.RUnlock()
+	copiedInvocations := map[string][][]interface{}{}
+	for key, value := range fake.invocations {
+		copiedInvocations[key] = value
+	}
+	return copiedInvocations
+}
+
+func (fake *FakePolicy) recordInvocation(key string, args []interface{}) {
+	fake.invocationsMutex.Lock()
+	defer fake.invocationsMutex.Unlock()
+	if fake.invocations == nil {
+		fake.invocations = map[string][][]interface{}{}
+	}
+	if fake.invocations[key] == nil {
+		fake.invocations[key] = [][]interface{}{}
+	}
+	fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ policies.Policy = new(FakePolicy)
diff --git a/internal/mode/static/policies/policiesfakes/fake_validator.go b/internal/mode/static/policies/policiesfakes/fake_validator.go
new file mode 100644
index 00000000..79a19826
--- /dev/null
+++ b/internal/mode/static/policies/policiesfakes/fake_validator.go
@@ -0,0 +1,187 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package policiesfakes
+
+import (
+	"sync"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+)
+
+type FakeValidator struct {
+	ConflictsStub        func(policies.Policy, policies.Policy) bool
+	conflictsMutex       sync.RWMutex
+	conflictsArgsForCall []struct {
+		arg1 policies.Policy
+		arg2 policies.Policy
+	}
+	conflictsReturns struct {
+		result1 bool
+	}
+	conflictsReturnsOnCall map[int]struct {
+		result1 bool
+	}
+	ValidateStub        func(policies.Policy) error
+	validateMutex       sync.RWMutex
+	validateArgsForCall []struct {
+		arg1 policies.Policy
+	}
+	validateReturns struct {
+		result1 error
+	}
+	validateReturnsOnCall map[int]struct {
+		result1 error
+	}
+	invocations      map[string][][]interface{}
+	invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeValidator) Conflicts(arg1 policies.Policy, arg2 policies.Policy) bool {
+	fake.conflictsMutex.Lock()
+	ret, specificReturn := fake.conflictsReturnsOnCall[len(fake.conflictsArgsForCall)]
+	fake.conflictsArgsForCall = append(fake.conflictsArgsForCall, struct {
+		arg1 policies.Policy
+		arg2 policies.Policy
+	}{arg1, arg2})
+	stub := fake.ConflictsStub
+	fakeReturns := fake.conflictsReturns
+	fake.recordInvocation("Conflicts", []interface{}{arg1, arg2})
+	fake.conflictsMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeValidator) ConflictsCallCount() int {
+	fake.conflictsMutex.RLock()
+	defer fake.conflictsMutex.RUnlock()
+	return len(fake.conflictsArgsForCall)
+}
+
+func (fake *FakeValidator) ConflictsCalls(stub func(policies.Policy, policies.Policy) bool) {
+	fake.conflictsMutex.Lock()
+	defer fake.conflictsMutex.Unlock()
+	fake.ConflictsStub = stub
+}
+
+func (fake *FakeValidator) ConflictsArgsForCall(i int) (policies.Policy, policies.Policy) {
+	fake.conflictsMutex.RLock()
+	defer fake.conflictsMutex.RUnlock()
+	argsForCall := fake.conflictsArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeValidator) ConflictsReturns(result1 bool) {
+	fake.conflictsMutex.Lock()
+	defer fake.conflictsMutex.Unlock()
+	fake.ConflictsStub = nil
+	fake.conflictsReturns = struct {
+		result1 bool
+	}{result1}
+}
+
+func (fake *FakeValidator) ConflictsReturnsOnCall(i int, result1 bool) {
+	fake.conflictsMutex.Lock()
+	defer fake.conflictsMutex.Unlock()
+	fake.ConflictsStub = nil
+	if fake.conflictsReturnsOnCall == nil {
+		fake.conflictsReturnsOnCall = make(map[int]struct {
+			result1 bool
+		})
+	}
+	fake.conflictsReturnsOnCall[i] = struct {
+		result1 bool
+	}{result1}
+}
+
+func (fake *FakeValidator) Validate(arg1 policies.Policy) error {
+	fake.validateMutex.Lock()
+	ret, specificReturn := fake.validateReturnsOnCall[len(fake.validateArgsForCall)]
+	fake.validateArgsForCall = append(fake.validateArgsForCall, struct {
+		arg1 policies.Policy
+	}{arg1})
+	stub := fake.ValidateStub
+	fakeReturns := fake.validateReturns
+	fake.recordInvocation("Validate", []interface{}{arg1})
+	fake.validateMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeValidator) ValidateCallCount() int {
+	fake.validateMutex.RLock()
+	defer fake.validateMutex.RUnlock()
+	return len(fake.validateArgsForCall)
+}
+
+func (fake *FakeValidator) ValidateCalls(stub func(policies.Policy) error) {
+	fake.validateMutex.Lock()
+	defer fake.validateMutex.Unlock()
+	fake.ValidateStub = stub
+}
+
+func (fake *FakeValidator) ValidateArgsForCall(i int) policies.Policy {
+	fake.validateMutex.RLock()
+	defer fake.validateMutex.RUnlock()
+	argsForCall := fake.validateArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeValidator) ValidateReturns(result1 error) {
+	fake.validateMutex.Lock()
+	defer fake.validateMutex.Unlock()
+	fake.ValidateStub = nil
+	fake.validateReturns = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeValidator) ValidateReturnsOnCall(i int, result1 error) {
+	fake.validateMutex.Lock()
+	defer fake.validateMutex.Unlock()
+	fake.ValidateStub = nil
+	if fake.validateReturnsOnCall == nil {
+		fake.validateReturnsOnCall = make(map[int]struct {
+			result1 error
+		})
+	}
+	fake.validateReturnsOnCall[i] = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeValidator) Invocations() map[string][][]interface{} {
+	fake.invocationsMutex.RLock()
+	defer fake.invocationsMutex.RUnlock()
+	fake.conflictsMutex.RLock()
+	defer fake.conflictsMutex.RUnlock()
+	fake.validateMutex.RLock()
+	defer fake.validateMutex.RUnlock()
+	copiedInvocations := map[string][][]interface{}{}
+	for key, value := range fake.invocations {
+		copiedInvocations[key] = value
+	}
+	return copiedInvocations
+}
+
+func (fake *FakeValidator) recordInvocation(key string, args []interface{}) {
+	fake.invocationsMutex.Lock()
+	defer fake.invocationsMutex.Unlock()
+	if fake.invocations == nil {
+		fake.invocations = map[string][][]interface{}{}
+	}
+	if fake.invocations[key] == nil {
+		fake.invocations[key] = [][]interface{}{}
+	}
+	fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ policies.Validator = new(FakeValidator)
diff --git a/internal/mode/static/policies/policy.go b/internal/mode/static/policies/policy.go
new file mode 100644
index 00000000..12135d8f
--- /dev/null
+++ b/internal/mode/static/policies/policy.go
@@ -0,0 +1,28 @@
+package policies
+
+import (
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+)
+
+//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
+
+// Policy is an extension of client.Object. It adds methods that are common among all NGF Policies.
+//
+//counterfeiter:generate . Policy
+type Policy interface {
+	GetTargetRef() v1alpha2.LocalPolicyTargetReference
+	GetPolicyStatus() v1alpha2.PolicyStatus
+	SetPolicyStatus(status v1alpha2.PolicyStatus)
+	client.Object
+}
+
+// ConfigGenerator generates a slice of bytes containing the configuration from a Policy.
+//
+//counterfeiter:generate . ConfigGenerator
+type ConfigGenerator interface {
+	Generate(policy Policy) []byte
+}
+
+// We generate a mock of ObjectKind so that we can create fake policies and set their GVKs.
+//counterfeiter:generate k8s.io/apimachinery/pkg/runtime/schema.ObjectKind
diff --git a/internal/mode/static/sort/sort.go b/internal/mode/static/sort/sort.go
index ee9db1b2..6f25f138 100644
--- a/internal/mode/static/sort/sort.go
+++ b/internal/mode/static/sort/sort.go
@@ -1,6 +1,9 @@
 package sort
 
-import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
 
 // LessObjectMeta compares two ObjectMetas according to the Gateway API conflict resolution guidelines.
 // See https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts
@@ -14,3 +17,21 @@ func LessObjectMeta(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool {
 
 	return meta1.CreationTimestamp.Before(&meta2.CreationTimestamp)
 }
+
+// LessClientObject compares two client.Objects and returns true if:
+// - the first object was created first,
+// - the objects were created at the same time, or
+// - the first object's name appears first in alphabetical order.
+func LessClientObject(obj1 client.Object, obj2 client.Object) bool {
+	create1 := obj1.GetCreationTimestamp()
+	create2 := obj2.GetCreationTimestamp()
+
+	if create1.Time.Equal(create2.Time) {
+		if obj1.GetNamespace() == obj2.GetNamespace() {
+			return obj1.GetName() < obj2.GetName()
+		}
+		return obj1.GetNamespace() < obj2.GetNamespace()
+	}
+
+	return create1.Time.Before(create2.Time)
+}
diff --git a/internal/mode/static/state/change_processor.go b/internal/mode/static/state/change_processor.go
index 0a57d8cd..e8342aa6 100644
--- a/internal/mode/static/state/change_processor.go
+++ b/internal/mode/static/state/change_processor.go
@@ -1,7 +1,6 @@
 package state
 
 import (
-	"fmt"
 	"sync"
 
 	"github.com/go-logr/logr"
@@ -9,18 +8,17 @@ import (
 	discoveryV1 "k8s.io/api/discovery/v1"
 	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/gateway-api/apis/v1alpha3"
 	"sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
 )
@@ -42,8 +40,6 @@ const (
 
 //counterfeiter:generate . ChangeProcessor
 
-type extractGVKFunc func(obj client.Object) schema.GroupVersionKind
-
 // ChangeProcessor processes the changes to resources and produces a graph-like representation
 // of the Gateway configuration. It only supports one GatewayClass resource.
 type ChangeProcessor interface {
@@ -68,8 +64,8 @@ type ChangeProcessorConfig struct {
 	Validators validation.Validators
 	// EventRecorder records events for Kubernetes resources.
 	EventRecorder record.EventRecorder
-	// Scheme is the Kubernetes scheme.
-	Scheme *runtime.Scheme
+	// MustExtractGVK is a function that extracts schema.GroupVersionKind from a client.Object.
+	MustExtractGVK kinds.MustExtractGVK
 	// ProtectedPorts are the ports that may not be configured by a listener with a descriptive name of the ports.
 	ProtectedPorts graph.ProtectedPorts
 	// Logger is the logger for this Change Processor.
@@ -110,14 +106,7 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl {
 		ConfigMaps:         make(map[types.NamespacedName]*apiv1.ConfigMap),
 		NginxProxies:       make(map[types.NamespacedName]*ngfAPI.NginxProxy),
 		GRPCRoutes:         make(map[types.NamespacedName]*v1.GRPCRoute),
-	}
-
-	extractGVK := func(obj client.Object) schema.GroupVersionKind {
-		gvk, err := apiutil.GVKForObject(obj, cfg.Scheme)
-		if err != nil {
-			panic(fmt.Errorf("failed to get GVK for object %T: %w", obj, err))
-		}
-		return gvk
+		NGFPolicies:        make(map[graph.PolicyKey]policies.Policy),
 	}
 
 	processor := &ChangeProcessorImpl{
@@ -129,74 +118,93 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl {
 		return processor.latestGraph != nil && processor.latestGraph.IsReferenced(obj, nsname)
 	}
 
+	isNGFPolicyRelevant := func(obj client.Object, nsname types.NamespacedName) bool {
+		pol, ok := obj.(policies.Policy)
+		if !ok {
+			return false
+		}
+
+		gvk := cfg.MustExtractGVK(obj)
+
+		return processor.latestGraph != nil && processor.latestGraph.IsNGFPolicyRelevant(pol, gvk, nsname)
+	}
+
+	// Use this object store for all NGF policies
+	commonPolicyObjectStore := newNGFPolicyObjectStore(clusterStore.NGFPolicies, cfg.MustExtractGVK)
+
 	trackingUpdater := newChangeTrackingUpdater(
-		extractGVK,
+		cfg.MustExtractGVK,
 		[]changeTrackingUpdaterObjectTypeCfg{
 			{
-				gvk:       extractGVK(&v1.GatewayClass{}),
+				gvk:       cfg.MustExtractGVK(&v1.GatewayClass{}),
 				store:     newObjectStoreMapAdapter(clusterStore.GatewayClasses),
 				predicate: nil,
 			},
 			{
-				gvk:       extractGVK(&v1.Gateway{}),
+				gvk:       cfg.MustExtractGVK(&v1.Gateway{}),
 				store:     newObjectStoreMapAdapter(clusterStore.Gateways),
 				predicate: nil,
 			},
 			{
-				gvk:       extractGVK(&v1.HTTPRoute{}),
+				gvk:       cfg.MustExtractGVK(&v1.HTTPRoute{}),
 				store:     newObjectStoreMapAdapter(clusterStore.HTTPRoutes),
 				predicate: nil,
 			},
 			{
-				gvk:       extractGVK(&v1beta1.ReferenceGrant{}),
+				gvk:       cfg.MustExtractGVK(&v1beta1.ReferenceGrant{}),
 				store:     newObjectStoreMapAdapter(clusterStore.ReferenceGrants),
 				predicate: nil,
 			},
 			{
-				gvk:       extractGVK(&v1alpha3.BackendTLSPolicy{}),
+				gvk:       cfg.MustExtractGVK(&v1alpha3.BackendTLSPolicy{}),
 				store:     newObjectStoreMapAdapter(clusterStore.BackendTLSPolicies),
 				predicate: nil,
 			},
 			{
-				gvk:       extractGVK(&v1.GRPCRoute{}),
+				gvk:       cfg.MustExtractGVK(&v1.GRPCRoute{}),
 				store:     newObjectStoreMapAdapter(clusterStore.GRPCRoutes),
 				predicate: nil,
 			},
 			{
-				gvk:       extractGVK(&apiv1.Namespace{}),
+				gvk:       cfg.MustExtractGVK(&apiv1.Namespace{}),
 				store:     newObjectStoreMapAdapter(clusterStore.Namespaces),
 				predicate: funcPredicate{stateChanged: isReferenced},
 			},
 			{
-				gvk:       extractGVK(&apiv1.Service{}),
+				gvk:       cfg.MustExtractGVK(&apiv1.Service{}),
 				store:     newObjectStoreMapAdapter(clusterStore.Services),
 				predicate: funcPredicate{stateChanged: isReferenced},
 			},
 			{
-				gvk:       extractGVK(&discoveryV1.EndpointSlice{}),
+				gvk:       cfg.MustExtractGVK(&discoveryV1.EndpointSlice{}),
 				store:     nil,
 				predicate: funcPredicate{stateChanged: isReferenced},
 			},
 			{
-				gvk:       extractGVK(&apiv1.Secret{}),
+				gvk:       cfg.MustExtractGVK(&apiv1.Secret{}),
 				store:     newObjectStoreMapAdapter(clusterStore.Secrets),
 				predicate: funcPredicate{stateChanged: isReferenced},
 			},
 			{
-				gvk:       extractGVK(&apiv1.ConfigMap{}),
+				gvk:       cfg.MustExtractGVK(&apiv1.ConfigMap{}),
 				store:     newObjectStoreMapAdapter(clusterStore.ConfigMaps),
 				predicate: funcPredicate{stateChanged: isReferenced},
 			},
 			{
-				gvk:       extractGVK(&apiext.CustomResourceDefinition{}),
+				gvk:       cfg.MustExtractGVK(&apiext.CustomResourceDefinition{}),
 				store:     newObjectStoreMapAdapter(clusterStore.CRDMetadata),
 				predicate: annotationChangedPredicate{annotation: gatewayclass.BundleVersionAnnotation},
 			},
 			{
-				gvk:       extractGVK(&ngfAPI.NginxProxy{}),
+				gvk:       cfg.MustExtractGVK(&ngfAPI.NginxProxy{}),
 				store:     newObjectStoreMapAdapter(clusterStore.NginxProxies),
 				predicate: funcPredicate{stateChanged: isReferenced},
 			},
+			{
+				gvk:       cfg.MustExtractGVK(&ngfAPI.ClientSettingsPolicy{}),
+				store:     commonPolicyObjectStore,
+				predicate: funcPredicate{stateChanged: isNGFPolicyRelevant},
+			},
 		},
 	)
 
diff --git a/internal/mode/static/state/conditions/conditions.go b/internal/mode/static/state/conditions/conditions.go
index 84d1cbaf..aa7c73ea 100644
--- a/internal/mode/static/state/conditions/conditions.go
+++ b/internal/mode/static/state/conditions/conditions.go
@@ -11,14 +11,6 @@ import (
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 )
 
-// BackendTLSPolicyConditionType is a type of condition associated with a BackendTLSPolicy.
-// This type should be used with the BackendTLSPolicyStatus.Conditions field.
-type BackendTLSPolicyConditionType string
-
-// BackendTLSPolicyConditionReason defines the set of reasons that explain why a particular BackendTLSPolicy condition
-// type has been raised.
-type BackendTLSPolicyConditionReason string
-
 const (
 	// ListenerReasonUnsupportedValue is used with the "Accepted" condition when a value of a field in a Listener
 	// is invalid or not supported.
@@ -605,19 +597,19 @@ func NewNginxGatewayInvalid(msg string) conditions.Condition {
 	}
 }
 
-// NewBackendTLSPolicyAccepted returns a Condition that indicates that the BackendTLSPolicy config is valid and accepted
-// by the Gateway.
-func NewBackendTLSPolicyAccepted() conditions.Condition {
+// NewPolicyAccepted returns a Condition that indicates that the Policy is accepted.
+func NewPolicyAccepted() conditions.Condition {
 	return conditions.Condition{
 		Type:    string(v1alpha2.PolicyConditionAccepted),
 		Status:  metav1.ConditionTrue,
 		Reason:  string(v1alpha2.PolicyReasonAccepted),
-		Message: "BackendTLSPolicy is accepted by the Gateway",
+		Message: "Policy is accepted",
 	}
 }
 
-// NewBackendTLSPolicyInvalid returns a Condition that indicates that the BackendTLSPolicy config is invalid.
-func NewBackendTLSPolicyInvalid(msg string) conditions.Condition {
+// NewPolicyInvalid returns a Condition that indicates that the Policy is not accepted because it is semantically or
+// syntactically invalid.
+func NewPolicyInvalid(msg string) conditions.Condition {
 	return conditions.Condition{
 		Type:    string(v1alpha2.PolicyConditionAccepted),
 		Status:  metav1.ConditionFalse,
@@ -625,3 +617,25 @@ func NewBackendTLSPolicyInvalid(msg string) conditions.Condition {
 		Message: msg,
 	}
 }
+
+// NewPolicyConflicted returns a Condition that indicates that the Policy is not accepted because it conflicts with
+// another Policy and a merge is not possible.
+func NewPolicyConflicted(msg string) conditions.Condition {
+	return conditions.Condition{
+		Type:    string(v1alpha2.PolicyConditionAccepted),
+		Status:  metav1.ConditionFalse,
+		Reason:  string(v1alpha2.PolicyReasonConflicted),
+		Message: msg,
+	}
+}
+
+// NewPolicyTargetNotFound returns a Condition that indicates that the Policy is not accepted because the target
+// resource does not exist or can not be attached to.
+func NewPolicyTargetNotFound(msg string) conditions.Condition {
+	return conditions.Condition{
+		Type:    string(v1alpha2.PolicyConditionAccepted),
+		Status:  metav1.ConditionFalse,
+		Reason:  string(v1alpha2.PolicyReasonTargetNotFound),
+		Message: msg,
+	}
+}
diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go
index 648d0a5c..a8ed937c 100644
--- a/internal/mode/static/state/dataplane/configuration.go
+++ b/internal/mode/static/state/dataplane/configuration.go
@@ -13,6 +13,7 @@ import (
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver"
 )
@@ -27,6 +28,7 @@ func BuildConfiguration(
 	ctx context.Context,
 	g *graph.Graph,
 	resolver resolver.ServiceResolver,
+	generator policies.ConfigGenerator,
 	configVersion int,
 ) Configuration {
 	if g.GatewayClass == nil || !g.GatewayClass.Valid {
@@ -38,7 +40,7 @@ func BuildConfiguration(
 	}
 
 	upstreams := buildUpstreams(ctx, g.Gateway.Listeners, resolver)
-	httpServers, sslServers := buildServers(g.Gateway.Listeners)
+	httpServers, sslServers := buildServers(g.Gateway, generator)
 	backendGroups := buildBackendGroups(append(httpServers, sslServers...))
 	keyPairs := buildSSLKeyPairs(g.ReferencedSecrets, g.Gateway.Listeners)
 	certBundles := buildCertBundles(g.ReferencedCaCertConfigMaps, backendGroups)
@@ -200,17 +202,17 @@ func convertBackendTLS(btp *graph.BackendTLSPolicy) *VerifyTLS {
 	return verify
 }
 
-func buildServers(listeners []*graph.Listener) (http, ssl []VirtualServer) {
+func buildServers(gw *graph.Gateway, generator policies.ConfigGenerator) (http, ssl []VirtualServer) {
 	rulesForProtocol := map[v1.ProtocolType]portPathRules{
 		v1.HTTPProtocolType:  make(portPathRules),
 		v1.HTTPSProtocolType: make(portPathRules),
 	}
 
-	for _, l := range listeners {
+	for _, l := range gw.Listeners {
 		if l.Valid {
 			rules := rulesForProtocol[l.Source.Protocol][l.Source.Port]
 			if rules == nil {
-				rules = newHostPathRules()
+				rules = newHostPathRules(generator)
 				rulesForProtocol[l.Source.Protocol][l.Source.Port] = rules
 			}
 
@@ -221,7 +223,19 @@ func buildServers(listeners []*graph.Listener) (http, ssl []VirtualServer) {
 	httpRules := rulesForProtocol[v1.HTTPProtocolType]
 	sslRules := rulesForProtocol[v1.HTTPSProtocolType]
 
-	return httpRules.buildServers(), sslRules.buildServers()
+	httpServers, sslServers := httpRules.buildServers(), sslRules.buildServers()
+
+	additions := buildAdditions(gw.Policies, generator)
+
+	for i := range httpServers {
+		httpServers[i].Additions = additions
+	}
+
+	for i := range sslServers {
+		sslServers[i].Additions = additions
+	}
+
+	return httpServers, sslServers
 }
 
 // portPathRules keeps track of hostPathRules per port
@@ -248,18 +262,20 @@ type pathAndType struct {
 }
 
 type hostPathRules struct {
+	generator        policies.ConfigGenerator
 	rulesPerHost     map[string]map[pathAndType]PathRule
 	listenersForHost map[string]*graph.Listener
 	httpsListeners   []*graph.Listener
-	listenersExist   bool
 	port             int32
+	listenersExist   bool
 }
 
-func newHostPathRules() *hostPathRules {
+func newHostPathRules(generator policies.ConfigGenerator) *hostPathRules {
 	return &hostPathRules{
 		rulesPerHost:     make(map[string]map[pathAndType]PathRule),
 		listenersForHost: make(map[string]*graph.Listener),
 		httpsListeners:   make([]*graph.Listener, 0),
+		generator:        generator,
 	}
 }
 
@@ -327,6 +343,8 @@ func (hpr *hostPathRules) upsertRoute(route *graph.L7Route, listener *graph.List
 			}
 		}
 
+		additions := buildAdditions(route.Policies, hpr.generator)
+
 		for _, h := range hostnames {
 			for _, m := range rule.Matches {
 				path := getPath(m.Path)
@@ -351,6 +369,7 @@ func (hpr *hostPathRules) upsertRoute(route *graph.L7Route, listener *graph.List
 					BackendGroup: newBackendGroup(rule.BackendRefs, routeNsName, i),
 					Filters:      filters,
 					Match:        convertMatch(m),
+					Additions:    additions,
 				})
 
 				hpr.rulesPerHost[h][key] = hostRule
@@ -637,3 +656,29 @@ func buildBaseHTTPConfig(g *graph.Graph) BaseHTTPConfig {
 
 	return baseConfig
 }
+
+func buildAdditions(policies []*graph.Policy, generator policies.ConfigGenerator) []Addition {
+	if len(policies) == 0 {
+		return nil
+	}
+
+	additions := make([]Addition, 0, len(policies))
+
+	for _, policy := range policies {
+		if !policy.Valid {
+			continue
+		}
+
+		additions = append(additions, Addition{
+			Bytes: generator.Generate(policy.Source),
+			Identifier: fmt.Sprintf(
+				"%s_%s_%s",
+				policy.Source.GetObjectKind().GroupVersionKind().Kind,
+				policy.Source.GetNamespace(),
+				policy.Source.GetName(),
+			),
+		})
+	}
+
+	return additions
+}
diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go
index 23ab0a46..7e7feb8f 100644
--- a/internal/mode/static/state/dataplane/types.go
+++ b/internal/mode/static/state/dataplane/types.go
@@ -68,10 +68,20 @@ type VirtualServer struct {
 	Hostname string
 	// PathRules is a collection of routing rules.
 	PathRules []PathRule
-	// IsDefault indicates whether the server is the default server.
-	IsDefault bool
+	// Additions is a list of config additions for the server.
+	Additions []Addition
 	// Port is the port of the server.
 	Port int32
+	// IsDefault indicates whether the server is the default server.
+	IsDefault bool
+}
+
+// Addition holds additional configuration.
+type Addition struct {
+	// Identifier is a unique identifier for the addition.
+	Identifier string
+	// Bytes contains the additional configuration.
+	Bytes []byte
 }
 
 // Upstream is a pool of endpoints to be load balanced.
@@ -204,6 +214,8 @@ type MatchRule struct {
 	Source *metav1.ObjectMeta
 	// Match holds the match for the rule.
 	Match Match
+	// Additions holds the config additions for the rule.
+	Additions []Addition
 	// BackendGroup is the group of Backends that the rule routes to.
 	BackendGroup BackendGroup
 }
diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go
index 172f5d5a..5e363389 100644
--- a/internal/mode/static/state/graph/backend_refs.go
+++ b/internal/mode/static/state/graph/backend_refs.go
@@ -247,7 +247,7 @@ func findBackendTLSPolicyForService(
 		for _, targetRef := range btp.Source.Spec.TargetRefs {
 			if string(targetRef.Name) == refName && btpNs == refNs {
 				if beTLSPolicy != nil {
-					if sort.LessObjectMeta(&btp.Source.ObjectMeta, &beTLSPolicy.Source.ObjectMeta) {
+					if sort.LessClientObject(btp.Source, beTLSPolicy.Source) {
 						beTLSPolicy = btp
 					}
 				} else {
@@ -263,7 +263,7 @@ func findBackendTLSPolicyForService(
 		if !beTLSPolicy.Valid {
 			err = fmt.Errorf("the backend TLS policy is invalid: %s", beTLSPolicy.Conditions[0].Message)
 		} else {
-			beTLSPolicy.Conditions = append(beTLSPolicy.Conditions, staticConds.NewBackendTLSPolicyAccepted())
+			beTLSPolicy.Conditions = append(beTLSPolicy.Conditions, staticConds.NewPolicyAccepted())
 		}
 	}
 
diff --git a/internal/mode/static/state/graph/backend_tls_policy.go b/internal/mode/static/state/graph/backend_tls_policy.go
index 7749970c..1545abf6 100644
--- a/internal/mode/static/state/graph/backend_tls_policy.go
+++ b/internal/mode/static/state/graph/backend_tls_policy.go
@@ -1,16 +1,13 @@
 package graph
 
 import (
-	"errors"
 	"fmt"
 
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/validation/field"
-	v1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/gateway-api/apis/v1alpha3"
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
-	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 )
 
@@ -44,12 +41,8 @@ func processBackendTLSPolicies(
 	processedBackendTLSPolicies := make(map[types.NamespacedName]*BackendTLSPolicy, len(backendTLSPolicies))
 	for nsname, backendTLSPolicy := range backendTLSPolicies {
 		var caCertRef types.NamespacedName
-		valid, ignored, conds := validateBackendTLSPolicy(
-			backendTLSPolicy,
-			configMapResolver,
-			ctlrName,
-			gateway,
-		)
+
+		valid, ignored, conds := validateBackendTLSPolicy(backendTLSPolicy, configMapResolver, ctlrName)
 
 		if valid && !ignored && backendTLSPolicy.Spec.Validation.CACertificateRefs != nil {
 			caCertRef = types.NamespacedName{
@@ -76,17 +69,19 @@ func validateBackendTLSPolicy(
 	backendTLSPolicy *v1alpha3.BackendTLSPolicy,
 	configMapResolver *configMapResolver,
 	ctlrName string,
-	gateway *Gateway,
 ) (valid, ignored bool, conds []conditions.Condition) {
 	valid = true
 	ignored = false
-	if err := validateAncestorMaxCount(backendTLSPolicy, ctlrName, gateway); err != nil {
+
+	// FIXME (kate-osborn): https://github.com/nginxinc/nginx-gateway-fabric/issues/1987
+	if ancestorsFull(backendTLSPolicy.Status.Ancestors, ctlrName) {
 		valid = false
 		ignored = true
 	}
+
 	if err := validateBackendTLSHostname(backendTLSPolicy); err != nil {
 		valid = false
-		conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(fmt.Sprintf("invalid hostname: %s", err.Error())))
+		conds = append(conds, staticConds.NewPolicyInvalid(fmt.Sprintf("invalid hostname: %s", err.Error())))
 	}
 
 	caCertRefs := backendTLSPolicy.Spec.Validation.CACertificateRefs
@@ -94,49 +89,26 @@ func validateBackendTLSPolicy(
 	if len(caCertRefs) > 0 && wellKnownCerts != nil {
 		valid = false
 		msg := "CACertificateRefs and WellKnownCACertificates are mutually exclusive"
-		conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(msg))
+		conds = append(conds, staticConds.NewPolicyInvalid(msg))
 	} else if len(caCertRefs) > 0 {
 		if err := validateBackendTLSCACertRef(backendTLSPolicy, configMapResolver); err != nil {
 			valid = false
-			conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(
+			conds = append(conds, staticConds.NewPolicyInvalid(
 				fmt.Sprintf("invalid CACertificateRef: %s", err.Error())))
 		}
 	} else if wellKnownCerts != nil {
 		if err := validateBackendTLSWellKnownCACerts(backendTLSPolicy); err != nil {
 			valid = false
-			conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(
+			conds = append(conds, staticConds.NewPolicyInvalid(
 				fmt.Sprintf("invalid WellKnownCACertificates: %s", err.Error())))
 		}
 	} else {
 		valid = false
-		conds = append(conds, staticConds.NewBackendTLSPolicyInvalid("CACertRefs and WellKnownCACerts are both nil"))
+		conds = append(conds, staticConds.NewPolicyInvalid("CACertRefs and WellKnownCACerts are both nil"))
 	}
 	return valid, ignored, conds
 }
 
-func validateAncestorMaxCount(backendTLSPolicy *v1alpha3.BackendTLSPolicy, ctlrName string, gateway *Gateway) error {
-	var err error
-	if len(backendTLSPolicy.Status.Ancestors) >= 16 {
-		// check if we already are an ancestor on this policy. If we are, we are safe to continue.
-		ancestorRef := v1.ParentReference{
-			Namespace: helpers.GetPointer((v1.Namespace)(gateway.Source.Namespace)),
-			Name:      v1.ObjectName(gateway.Source.Name),
-		}
-		var alreadyAncestor bool
-		for _, ancestor := range backendTLSPolicy.Status.Ancestors {
-			if string(ancestor.ControllerName) == ctlrName && ancestor.AncestorRef.Name == ancestorRef.Name &&
-				ancestor.AncestorRef.Namespace != nil && *ancestor.AncestorRef.Namespace == *ancestorRef.Namespace {
-				alreadyAncestor = true
-				break
-			}
-		}
-		if !alreadyAncestor {
-			err = errors.New("too many ancestors, cannot attach a new Gateway")
-		}
-	}
-	return err
-}
-
 func validateBackendTLSHostname(btp *v1alpha3.BackendTLSPolicy) error {
 	h := string(btp.Spec.Validation.Hostname)
 
@@ -159,12 +131,16 @@ func validateBackendTLSCACertRef(btp *v1alpha3.BackendTLSPolicy, configMapResolv
 		valErr := field.NotSupported(path, btp.Spec.Validation.CACertificateRefs[0].Kind, []string{"ConfigMap"})
 		return valErr
 	}
-	if btp.Spec.Validation.CACertificateRefs[0].Group != "" && btp.Spec.Validation.CACertificateRefs[0].Group != "core" {
+	if btp.Spec.Validation.CACertificateRefs[0].Group != "" &&
+		btp.Spec.Validation.CACertificateRefs[0].Group != "core" {
 		path := field.NewPath("tls.cacertrefs[0].group")
 		valErr := field.NotSupported(path, btp.Spec.Validation.CACertificateRefs[0].Group, []string{"", "core"})
 		return valErr
 	}
-	nsName := types.NamespacedName{Namespace: btp.Namespace, Name: string(btp.Spec.Validation.CACertificateRefs[0].Name)}
+	nsName := types.NamespacedName{
+		Namespace: btp.Namespace,
+		Name:      string(btp.Spec.Validation.CACertificateRefs[0].Name),
+	}
 	if err := configMapResolver.resolve(nsName); err != nil {
 		path := field.NewPath("tls.cacertrefs[0]")
 		return field.Invalid(path, btp.Spec.Validation.CACertificateRefs[0], err.Error())
diff --git a/internal/mode/static/state/graph/gateway.go b/internal/mode/static/state/graph/gateway.go
index 79f0ae9d..6a826f80 100644
--- a/internal/mode/static/state/graph/gateway.go
+++ b/internal/mode/static/state/graph/gateway.go
@@ -21,6 +21,8 @@ type Gateway struct {
 	Listeners []*Listener
 	// Conditions holds the conditions for the Gateway.
 	Conditions []conditions.Condition
+	// Policies holds the policies attached to the Gateway.
+	Policies []*Policy
 	// Valid indicates whether the Gateway Spec is valid.
 	Valid bool
 }
@@ -75,7 +77,7 @@ func processGateways(
 	}
 
 	sort.Slice(referencedGws, func(i, j int) bool {
-		return ngfsort.LessObjectMeta(&referencedGws[i].ObjectMeta, &referencedGws[j].ObjectMeta)
+		return ngfsort.LessClientObject(referencedGws[i], referencedGws[j])
 	})
 
 	ignoredGws := make(map[types.NamespacedName]*v1.Gateway)
diff --git a/internal/mode/static/state/graph/gateway_listener.go b/internal/mode/static/state/graph/gateway_listener.go
index 432888ba..7c61d452 100644
--- a/internal/mode/static/state/graph/gateway_listener.go
+++ b/internal/mode/static/state/graph/gateway_listener.go
@@ -11,6 +11,7 @@ import (
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 )
 
@@ -230,7 +231,7 @@ func getAndValidateListenerSupportedKinds(listener v1.Listener) (
 	if listener.AllowedRoutes == nil || listener.AllowedRoutes.Kinds == nil {
 		return nil, []v1.RouteGroupKind{
 			{
-				Kind: "HTTPRoute",
+				Kind: kinds.HTTPRoute,
 			},
 		}
 	}
@@ -239,7 +240,7 @@ func getAndValidateListenerSupportedKinds(listener v1.Listener) (
 	supportedKinds := make([]v1.RouteGroupKind, 0, len(listener.AllowedRoutes.Kinds))
 
 	validHTTPProtocolRouteKind := func(kind v1.RouteGroupKind) bool {
-		if kind.Kind != v1.Kind("HTTPRoute") && kind.Kind != v1.Kind("GRPCRoute") {
+		if kind.Kind != v1.Kind(kinds.HTTPRoute) && kind.Kind != v1.Kind(kinds.GRPCRoute) {
 			return false
 		}
 		if kind.Group == nil || *kind.Group != v1.GroupName {
diff --git a/internal/mode/static/state/graph/gatewayclass.go b/internal/mode/static/state/graph/gatewayclass.go
index 23bbb078..d54e14e6 100644
--- a/internal/mode/static/state/graph/gatewayclass.go
+++ b/internal/mode/static/state/graph/gatewayclass.go
@@ -12,6 +12,7 @@ import (
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
 )
@@ -93,7 +94,7 @@ func validateGatewayClass(
 		var err error
 		path := field.NewPath("spec").Child("parametersRef")
 		if _, ok := supportedParamKinds[string(gc.Spec.ParametersRef.Kind)]; !ok {
-			err = field.NotSupported(path.Child("kind"), string(gc.Spec.ParametersRef.Kind), []string{"NginxProxy"})
+			err = field.NotSupported(path.Child("kind"), string(gc.Spec.ParametersRef.Kind), []string{kinds.NginxProxy})
 		} else if npCfg == nil {
 			err = field.NotFound(path.Child("name"), gc.Spec.ParametersRef.Name)
 			conds = append(conds, staticConds.NewGatewayClassRefNotFound())
@@ -117,5 +118,5 @@ func validateGatewayClass(
 }
 
 var supportedParamKinds = map[string]struct{}{
-	"NginxProxy": {},
+	kinds.NginxProxy: {},
 }
diff --git a/internal/mode/static/state/graph/graph.go b/internal/mode/static/state/graph/graph.go
index 2e23bd25..610ea4dc 100644
--- a/internal/mode/static/state/graph/graph.go
+++ b/internal/mode/static/state/graph/graph.go
@@ -4,14 +4,18 @@ import (
 	v1 "k8s.io/api/core/v1"
 	discoveryV1 "k8s.io/api/discovery/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
 	"sigs.k8s.io/gateway-api/apis/v1alpha3"
 	"sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
 )
 
@@ -29,6 +33,7 @@ type ClusterState struct {
 	ConfigMaps         map[types.NamespacedName]*v1.ConfigMap
 	NginxProxies       map[types.NamespacedName]*ngfAPI.NginxProxy
 	GRPCRoutes         map[types.NamespacedName]*gatewayv1.GRPCRoute
+	NGFPolicies        map[PolicyKey]policies.Policy
 }
 
 // Graph is a Graph-like representation of Gateway API resources.
@@ -63,6 +68,8 @@ type Graph struct {
 	BackendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy
 	// NginxProxy holds the NginxProxy config for the GatewayClass.
 	NginxProxy *ngfAPI.NginxProxy
+	// NGFPolicies holds all NGF Policies.
+	NGFPolicies map[PolicyKey]*Policy
 }
 
 // ProtectedPorts are the ports that may not be configured by a listener with a descriptive name of each port.
@@ -112,6 +119,54 @@ func (g *Graph) IsReferenced(resourceType client.Object, nsname types.Namespaced
 	}
 }
 
+// IsNGFPolicyRelevant returns whether the NGF Policy is a part of the Graph, or targets a resource in the Graph.
+func (g *Graph) IsNGFPolicyRelevant(
+	policy policies.Policy,
+	gvk schema.GroupVersionKind,
+	nsname types.NamespacedName,
+) bool {
+	key := PolicyKey{
+		NsName: nsname,
+		GVK:    gvk,
+	}
+
+	if _, exists := g.NGFPolicies[key]; exists {
+		return true
+	}
+
+	if policy == nil {
+		panic("policy cannot be nil")
+	}
+
+	ref := policy.GetTargetRef()
+
+	switch ref.Group {
+	case gatewayv1.GroupName:
+		return g.gatewayAPIResourceExist(ref, policy.GetNamespace())
+	default:
+		return false
+	}
+}
+
+func (g *Graph) gatewayAPIResourceExist(ref v1alpha2.LocalPolicyTargetReference, policyNs string) bool {
+	refNsName := types.NamespacedName{Name: string(ref.Name), Namespace: policyNs}
+
+	switch kind := ref.Kind; kind {
+	case kinds.Gateway:
+		if g.Gateway == nil {
+			return false
+		}
+
+		return gatewayExists(refNsName, g.Gateway.Source, g.IgnoredGateways)
+	case kinds.HTTPRoute, kinds.GRPCRoute:
+		_, exists := g.Routes[routeKeyForKind(kind, refNsName)]
+		return exists
+
+	default:
+		return false
+	}
+}
+
 // BuildGraph builds a Graph from a state.
 func BuildGraph(
 	state ClusterState,
@@ -135,6 +190,7 @@ func BuildGraph(
 	processedGws := processGateways(state.Gateways, gcName)
 
 	refGrantResolver := newReferenceGrantResolver(state.ReferenceGrants)
+
 	gw := buildGateway(processedGws.Winner, secretResolver, gc, refGrantResolver, protectedPorts)
 
 	processedBackendTLSPolicies := processBackendTLSPolicies(
@@ -151,6 +207,7 @@ func BuildGraph(
 		processedGws.GetAllNsNames(),
 		npCfg,
 	)
+
 	bindRoutesToListeners(routes, gw, state.Namespaces)
 	addBackendRefsToRouteRules(routes, refGrantResolver, state.Services, processedBackendTLSPolicies)
 
@@ -158,6 +215,8 @@ func BuildGraph(
 
 	referencedServices := buildReferencedServices(routes)
 
+	processedPolicies := processPolicies(state.NGFPolicies, validators.PolicyValidator, processedGws, routes)
+
 	g := &Graph{
 		GatewayClass:               gc,
 		Gateway:                    gw,
@@ -170,7 +229,28 @@ func BuildGraph(
 		ReferencedCaCertConfigMaps: configMapResolver.getResolvedConfigMaps(),
 		BackendTLSPolicies:         processedBackendTLSPolicies,
 		NginxProxy:                 npCfg,
+		NGFPolicies:                processedPolicies,
 	}
 
+	g.attachPolicies(controllerName)
+
 	return g
 }
+
+func gatewayExists(
+	gwNsName types.NamespacedName,
+	winner *gatewayv1.Gateway,
+	ignored map[types.NamespacedName]*gatewayv1.Gateway,
+) bool {
+	if winner == nil {
+		return false
+	}
+
+	if client.ObjectKeyFromObject(winner) == gwNsName {
+		return true
+	}
+
+	_, exists := ignored[gwNsName]
+
+	return exists
+}
diff --git a/internal/mode/static/state/graph/nginxproxy.go b/internal/mode/static/state/graph/nginxproxy.go
index 327d6cca..7df28d26 100644
--- a/internal/mode/static/state/graph/nginxproxy.go
+++ b/internal/mode/static/state/graph/nginxproxy.go
@@ -6,6 +6,7 @@ import (
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
 )
 
@@ -30,7 +31,7 @@ func isNginxProxyReferenced(npNSName types.NamespacedName, gc *GatewayClass) boo
 func gcReferencesAnyNginxProxy(gc *v1.GatewayClass) bool {
 	if gc != nil {
 		ref := gc.Spec.ParametersRef
-		return ref != nil && ref.Group == ngfAPI.GroupName && ref.Kind == v1.Kind("NginxProxy")
+		return ref != nil && ref.Group == ngfAPI.GroupName && ref.Kind == v1.Kind(kinds.NginxProxy)
 	}
 
 	return false
@@ -49,7 +50,10 @@ func validateNginxProxy(
 		telPath := spec.Child("telemetry")
 		if telemetry.ServiceName != nil {
 			if err := validator.ValidateServiceName(*telemetry.ServiceName); err != nil {
-				allErrs = append(allErrs, field.Invalid(telPath.Child("serviceName"), *telemetry.ServiceName, err.Error()))
+				allErrs = append(
+					allErrs,
+					field.Invalid(telPath.Child("serviceName"), *telemetry.ServiceName, err.Error()),
+				)
 			}
 		}
 
diff --git a/internal/mode/static/state/graph/policies.go b/internal/mode/static/state/graph/policies.go
new file mode 100644
index 00000000..e8cbb50a
--- /dev/null
+++ b/internal/mode/static/state/graph/policies.go
@@ -0,0 +1,287 @@
+package graph
+
+import (
+	"fmt"
+	"sort"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	ngfsort "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/sort"
+	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
+)
+
+// Policy represents an NGF Policy.
+type Policy struct {
+	// Source is the corresponding Policy resource.
+	Source policies.Policy
+	// Ancestor is the ancestor object of the Policy. Used in status.
+	Ancestor *PolicyAncestor
+	// TargetRef is the resource that the Policy targets.
+	TargetRef PolicyTargetRef
+	// Conditions holds the conditions for the Policy.
+	// These conditions apply to the entire Policy.
+	// The conditions in the Ancestor apply only to the Policy in regard to the Ancestor.
+	Conditions []conditions.Condition
+	// Valid indicates whether the Policy is valid.
+	Valid bool
+}
+
+// PolicyAncestor represents an ancestor of a Policy.
+type PolicyAncestor struct {
+	// Ancestor is the ancestor object.
+	Ancestor v1.ParentReference
+	// Conditions contains the list of conditions of the Policy in relation to the ancestor.
+	Conditions []conditions.Condition
+}
+
+// PolicyTargetRef represents the object that the Policy is targeting.
+type PolicyTargetRef struct {
+	// Kind is the Kind of the object.
+	Kind v1.Kind
+	// Group is the Group of the object.
+	Group v1.Group
+	// Nsname is the NamespacedName of the object.
+	Nsname types.NamespacedName
+}
+
+// PolicyKey is a unique identifier for an NGF Policy.
+type PolicyKey struct {
+	// Nsname is the NamespacedName of the Policy.
+	NsName types.NamespacedName
+	// GVK is the GroupVersionKind of the Policy.
+	GVK schema.GroupVersionKind
+}
+
+const (
+	gatewayGroupKind = v1.GroupName + "/" + kinds.Gateway
+	hrGroupKind      = v1.GroupName + "/" + kinds.HTTPRoute
+	grpcGroupKind    = v1.GroupName + "/" + kinds.GRPCRoute
+)
+
+// attachPolicies attaches the graph's processed policies to the resources they target. It modifies the graph in place.
+func (g *Graph) attachPolicies(ctlrName string) {
+	if g.Gateway == nil {
+		return
+	}
+
+	for _, policy := range g.NGFPolicies {
+		ref := policy.TargetRef
+
+		switch ref.Kind {
+		case kinds.Gateway:
+			attachPolicyToGateway(policy, g.Gateway, g.IgnoredGateways, ctlrName)
+		case kinds.HTTPRoute, kinds.GRPCRoute:
+			route, exists := g.Routes[routeKeyForKind(ref.Kind, ref.Nsname)]
+			if !exists {
+				continue
+			}
+
+			attachPolicyToRoute(policy, route, ctlrName)
+		}
+	}
+}
+
+func attachPolicyToRoute(policy *Policy, route *L7Route, ctlrName string) {
+	kind := v1.Kind(kinds.HTTPRoute)
+	if route.RouteType == RouteTypeGRPC {
+		kind = kinds.GRPCRoute
+	}
+
+	routeNsName := types.NamespacedName{Namespace: route.Source.GetNamespace(), Name: route.Source.GetName()}
+
+	ancestor := &PolicyAncestor{
+		Ancestor: createParentReference(v1.GroupName, kind, routeNsName),
+	}
+
+	curAncestorStatus := policy.Source.GetPolicyStatus().Ancestors
+	if ancestorsFull(curAncestorStatus, ctlrName) {
+		// FIXME (kate-osborn): https://github.com/nginxinc/nginx-gateway-fabric/issues/1987
+		return
+	}
+
+	policy.Ancestor = ancestor
+
+	if !route.Valid || !route.Attachable || len(route.ParentRefs) == 0 {
+		policy.Ancestor.Conditions = []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")}
+
+		return
+	}
+
+	route.Policies = append(route.Policies, policy)
+}
+
+func attachPolicyToGateway(
+	policy *Policy,
+	gw *Gateway,
+	ignoredGateways map[types.NamespacedName]*v1.Gateway,
+	ctlrName string,
+) {
+	ref := policy.TargetRef
+
+	_, ignored := ignoredGateways[ref.Nsname]
+
+	if !ignored && ref.Nsname != client.ObjectKeyFromObject(gw.Source) {
+		return
+	}
+
+	ancestor := &PolicyAncestor{
+		Ancestor: createParentReference(v1.GroupName, kinds.Gateway, ref.Nsname),
+	}
+
+	curAncestorStatus := policy.Source.GetPolicyStatus().Ancestors
+	if ancestorsFull(curAncestorStatus, ctlrName) {
+		// FIXME (kate-osborn): https://github.com/nginxinc/nginx-gateway-fabric/issues/1987
+		return
+	}
+
+	policy.Ancestor = ancestor
+
+	if ignored {
+		policy.Ancestor.Conditions = []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is ignored")}
+		return
+	}
+
+	if !gw.Valid {
+		policy.Ancestor.Conditions = []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")}
+		return
+	}
+
+	gw.Policies = append(gw.Policies, policy)
+}
+
+func processPolicies(
+	policies map[PolicyKey]policies.Policy,
+	validator validation.PolicyValidator,
+	gateways processedGateways,
+	routes map[RouteKey]*L7Route,
+) map[PolicyKey]*Policy {
+	if len(policies) == 0 || gateways.Winner == nil {
+		return nil
+	}
+
+	processedPolicies := make(map[PolicyKey]*Policy)
+
+	for key, policy := range policies {
+		ref := policy.GetTargetRef()
+		refNsName := types.NamespacedName{Name: string(ref.Name), Namespace: policy.GetNamespace()}
+
+		refGroupKind := fmt.Sprintf("%s/%s", ref.Group, ref.Kind)
+
+		switch refGroupKind {
+		case gatewayGroupKind:
+			if !gatewayExists(refNsName, gateways.Winner, gateways.Ignored) {
+				continue
+			}
+		case hrGroupKind, grpcGroupKind:
+			if _, exists := routes[routeKeyForKind(ref.Kind, refNsName)]; !exists {
+				continue
+			}
+		default:
+			continue
+		}
+
+		var conds []conditions.Condition
+
+		if err := validator.Validate(policy); err != nil {
+			conds = append(conds, staticConds.NewPolicyInvalid(err.Error()))
+		}
+
+		processedPolicies[key] = &Policy{
+			Source:     policy,
+			Valid:      len(conds) == 0,
+			Conditions: conds,
+			TargetRef: PolicyTargetRef{
+				Kind:   ref.Kind,
+				Group:  ref.Group,
+				Nsname: refNsName,
+			},
+		}
+	}
+
+	markConflictedPolicies(processedPolicies, validator)
+
+	return processedPolicies
+}
+
+// markConflictedPolicies marks policies that conflict with a policy of greater precedence as invalid.
+// Policies are sorted by timestamp and then alphabetically.
+func markConflictedPolicies(policies map[PolicyKey]*Policy, validator validation.PolicyValidator) {
+	// Policies can only conflict if they are the same policy type (gvk) and they target the same resource.
+	type key struct {
+		policyGVK schema.GroupVersionKind
+		PolicyTargetRef
+	}
+
+	possibles := make(map[key][]*Policy)
+
+	for pk, p := range policies {
+		// If a policy is invalid, it cannot conflict with another policy.
+		if p.Valid {
+			ak := key{
+				PolicyTargetRef: p.TargetRef,
+				policyGVK:       pk.GVK,
+			}
+			if possibles[ak] == nil {
+				possibles[ak] = make([]*Policy, 0)
+			}
+			possibles[ak] = append(possibles[ak], p)
+		}
+	}
+
+	for _, policyList := range possibles {
+		if len(policyList) == 1 {
+			// if the policyList only has one entry, then we don't need to check for conflicts.
+			continue
+		}
+
+		// First, we sort the policyList according to the rules in the spec.
+		// This will put them in priority-order.
+		sort.Slice(
+			policyList, func(i, j int) bool {
+				return ngfsort.LessClientObject(policyList[i].Source, policyList[j].Source)
+			},
+		)
+
+		// Second, we range over the policyList, starting with the highest priority policy.
+		for i := range policyList {
+			if !policyList[i].Valid {
+				// Ignore policy that has already been marked as invalid.
+				continue
+			}
+
+			// Next, we compare the ith policy (policyList[i]) to the rest of the policies in the list.
+			// The ith policy takes precedence over polices that follow it, so if there is a conflict between
+			// it and a subsequent policy, the ith policy wins, and we mark the subsequent policy as invalid.
+			// Example: policyList = [A, B, C] where B conflicts with A.
+			// i=A, j=B => conflict, B's marked as invalid.
+			// i=A, j=C => no conflict.
+			// i=B, j=C => B's already invalid, so we hit the continue.
+			// i=C => j loop terminates.
+			// Results: A, and C are valid. B is invalid.
+			for j := i + 1; j < len(policyList); j++ {
+				if !policyList[j].Valid {
+					// Ignore policy that has already been marked as invalid.
+					continue
+				}
+
+				if validator.Conflicts(policyList[i].Source, policyList[j].Source) {
+					conflicted := policyList[j]
+					conflicted.Valid = false
+					conflicted.Conditions = append(conflicted.Conditions, staticConds.NewPolicyConflicted(
+						fmt.Sprintf(
+							"Conflicts with another %s",
+							conflicted.Source.GetObjectKind().GroupVersionKind().Kind,
+						),
+					))
+				}
+			}
+		}
+	}
+}
diff --git a/internal/mode/static/state/graph/policy_ancestor.go b/internal/mode/static/state/graph/policy_ancestor.go
new file mode 100644
index 00000000..b3733541
--- /dev/null
+++ b/internal/mode/static/state/graph/policy_ancestor.go
@@ -0,0 +1,39 @@
+package graph
+
+import (
+	"k8s.io/apimachinery/pkg/types"
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+)
+
+const maxAncestors = 16
+
+func ancestorsFull(
+	ancestors []v1alpha2.PolicyAncestorStatus,
+	ctlrName string,
+) bool {
+	if len(ancestors) < maxAncestors {
+		return false
+	}
+
+	for _, ancestor := range ancestors {
+		if string(ancestor.ControllerName) == ctlrName {
+			return false
+		}
+	}
+
+	return true
+}
+
+func createParentReference(
+	group v1.Group,
+	kind v1.Kind,
+	nsname types.NamespacedName,
+) v1.ParentReference {
+	return v1.ParentReference{
+		Group:     &group,
+		Kind:      &kind,
+		Namespace: (*v1.Namespace)(&nsname.Namespace),
+		Name:      v1.ObjectName(nsname.Name),
+	}
+}
diff --git a/internal/mode/static/state/graph/reference_grant.go b/internal/mode/static/state/graph/reference_grant.go
index c0db7027..b833825d 100644
--- a/internal/mode/static/state/graph/reference_grant.go
+++ b/internal/mode/static/state/graph/reference_grant.go
@@ -4,6 +4,8 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 	v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 )
 
 // referenceGrantResolver resolves references from one resource to another.
@@ -58,7 +60,7 @@ func toService(nsname types.NamespacedName) toResource {
 func fromGateway(namespace string) fromResource {
 	return fromResource{
 		group:     v1.GroupName,
-		kind:      "Gateway",
+		kind:      kinds.Gateway,
 		namespace: namespace,
 	}
 }
@@ -66,7 +68,7 @@ func fromGateway(namespace string) fromResource {
 func fromHTTPRoute(namespace string) fromResource {
 	return fromResource{
 		group:     v1.GroupName,
-		kind:      "HTTPRoute",
+		kind:      kinds.HTTPRoute,
 		namespace: namespace,
 	}
 }
diff --git a/internal/mode/static/state/graph/route_common.go b/internal/mode/static/state/graph/route_common.go
index 7fdbaee4..c68152fd 100644
--- a/internal/mode/static/state/graph/route_common.go
+++ b/internal/mode/static/state/graph/route_common.go
@@ -13,6 +13,7 @@ import (
 
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
 )
@@ -57,8 +58,10 @@ const (
 
 // RouteKey is the unique identifier for a L7Route
 type RouteKey struct {
+	// NamespacedName is the NamespacedName of the Route.
 	NamespacedName types.NamespacedName
-	RouteType      RouteType
+	// RouteType is the type of the Route.
+	RouteType RouteType
 }
 
 // L7Route is the generic type for the layer 7 routes, HTTPRoute and GRPCRoute
@@ -73,6 +76,8 @@ type L7Route struct {
 	ParentRefs []ParentRef
 	// Conditions define the conditions to be reported in the status of the Route.
 	Conditions []conditions.Condition
+	// Policies holds the policies that are attached to the Route.
+	Policies []*Policy
 	// Valid indicates if the Route is valid.
 	Valid bool
 	// Attachable indicates if the Route is attachable to any Listener.
@@ -218,7 +223,7 @@ func findGatewayForParentRef(
 	routeNamespace string,
 	gatewayNsNames []types.NamespacedName,
 ) (gwNsName types.NamespacedName, found bool) {
-	if ref.Kind != nil && *ref.Kind != "Gateway" {
+	if ref.Kind != nil && *ref.Kind != kinds.Gateway {
 		return types.NamespacedName{}, false
 	}
 	if ref.Group != nil && *ref.Group != v1.GroupName {
@@ -698,7 +703,8 @@ func validateResponseHeaders(
 	for _, h := range headers {
 		valErr := field.Invalid(path, h, "header name is not allowed")
 		name := strings.ToLower(string(h.Name))
-		if _, exists := disallowedResponseHeaderSet[name]; exists || strings.HasPrefix(name, strings.ToLower(invalidPrefix)) {
+		if _, exists := disallowedResponseHeaderSet[name]; exists ||
+			strings.HasPrefix(name, strings.ToLower(invalidPrefix)) {
 			allErrs = append(allErrs, valErr)
 		}
 	}
@@ -742,3 +748,17 @@ func validateRequestHeaderStringCaseInsensitiveUnique(headers []string, path *fi
 
 	return allErrs
 }
+
+func routeKeyForKind(kind v1.Kind, nsname types.NamespacedName) RouteKey {
+	key := RouteKey{NamespacedName: nsname}
+	switch kind {
+	case kinds.HTTPRoute:
+		key.RouteType = RouteTypeHTTP
+	case kinds.GRPCRoute:
+		key.RouteType = RouteTypeGRPC
+	default:
+		panic(fmt.Sprintf("unsupported route kind: %s", kind))
+	}
+
+	return key
+}
diff --git a/internal/mode/static/state/store.go b/internal/mode/static/state/store.go
index 02a25855..163d0519 100644
--- a/internal/mode/static/state/store.go
+++ b/internal/mode/static/state/store.go
@@ -7,6 +7,10 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
 )
 
 // Updater updates the cluster state.
@@ -17,9 +21,59 @@ type Updater interface {
 
 // objectStore is a store of client.Object
 type objectStore interface {
-	get(nsname types.NamespacedName) client.Object
+	get(objType client.Object, nsname types.NamespacedName) client.Object
 	upsert(obj client.Object)
-	delete(nsname types.NamespacedName)
+	delete(objType client.Object, nsname types.NamespacedName)
+}
+
+// ngfPolicyObjectStore is a store of policies.Policy.
+// A single store should be used to store all types of policies.Policy.
+type ngfPolicyObjectStore struct {
+	policies       map[graph.PolicyKey]policies.Policy
+	extractGVKFunc kinds.MustExtractGVK
+}
+
+// newNGFPolicyObjectStore returns a new ngfPolicyObjectStore.
+func newNGFPolicyObjectStore(
+	policies map[graph.PolicyKey]policies.Policy,
+	gvkFunc kinds.MustExtractGVK,
+) *ngfPolicyObjectStore {
+	return &ngfPolicyObjectStore{
+		policies:       policies,
+		extractGVKFunc: gvkFunc,
+	}
+}
+
+func (p *ngfPolicyObjectStore) get(objType client.Object, nsname types.NamespacedName) client.Object {
+	key := graph.PolicyKey{
+		NsName: nsname,
+		GVK:    p.extractGVKFunc(objType),
+	}
+
+	return p.policies[key]
+}
+
+func (p *ngfPolicyObjectStore) upsert(obj client.Object) {
+	key := graph.PolicyKey{
+		NsName: client.ObjectKeyFromObject(obj),
+		GVK:    p.extractGVKFunc(obj),
+	}
+
+	pol, ok := obj.(policies.Policy)
+	if !ok {
+		panic(fmt.Sprintf("expected NGF Policy, got %T", obj))
+	}
+
+	p.policies[key] = pol
+}
+
+func (p *ngfPolicyObjectStore) delete(objType client.Object, nsname types.NamespacedName) {
+	key := graph.PolicyKey{
+		NsName: nsname,
+		GVK:    p.extractGVKFunc(objType),
+	}
+
+	delete(p.policies, key)
 }
 
 // objectStoreMapAdapter wraps maps of types.NamespacedName to Kubernetes resources
@@ -34,7 +88,7 @@ func newObjectStoreMapAdapter[T client.Object](objects map[types.NamespacedName]
 	}
 }
 
-func (m *objectStoreMapAdapter[T]) get(nsname types.NamespacedName) client.Object {
+func (m *objectStoreMapAdapter[T]) get(_ client.Object, nsname types.NamespacedName) client.Object {
 	obj, exist := m.objects[nsname]
 	if !exist {
 		return nil
@@ -51,7 +105,7 @@ func (m *objectStoreMapAdapter[T]) upsert(obj client.Object) {
 	m.objects[client.ObjectKeyFromObject(obj)] = t
 }
 
-func (m *objectStoreMapAdapter[T]) delete(nsname types.NamespacedName) {
+func (m *objectStoreMapAdapter[T]) delete(_ client.Object, nsname types.NamespacedName) {
 	delete(m.objects, nsname)
 }
 
@@ -69,13 +123,13 @@ func (list gvkList) contains(gvk schema.GroupVersionKind) bool {
 
 type multiObjectStore struct {
 	stores        map[schema.GroupVersionKind]objectStore
-	extractGVK    extractGVKFunc
+	extractGVK    kinds.MustExtractGVK
 	persistedGVKs gvkList
 }
 
 func newMultiObjectStore(
 	stores map[schema.GroupVersionKind]objectStore,
-	extractGVK extractGVKFunc,
+	extractGVK kinds.MustExtractGVK,
 	persistedGVKs gvkList,
 ) *multiObjectStore {
 	return &multiObjectStore{
@@ -97,7 +151,7 @@ func (m *multiObjectStore) mustFindStoreForObj(obj client.Object) objectStore {
 }
 
 func (m *multiObjectStore) get(objType client.Object, nsname types.NamespacedName) client.Object {
-	return m.mustFindStoreForObj(objType).get(nsname)
+	return m.mustFindStoreForObj(objType).get(objType, nsname)
 }
 
 func (m *multiObjectStore) upsert(obj client.Object) {
@@ -105,7 +159,7 @@ func (m *multiObjectStore) upsert(obj client.Object) {
 }
 
 func (m *multiObjectStore) delete(objType client.Object, nsname types.NamespacedName) {
-	m.mustFindStoreForObj(objType).delete(nsname)
+	m.mustFindStoreForObj(objType).delete(objType, nsname)
 }
 
 func (m *multiObjectStore) persists(objTypeGVK schema.GroupVersionKind) bool {
@@ -130,14 +184,14 @@ type changeTrackingUpdater struct {
 	store                  *multiObjectStore
 	stateChangedPredicates map[schema.GroupVersionKind]stateChangedPredicate
 
-	extractGVK    extractGVKFunc
+	extractGVK    kinds.MustExtractGVK
 	supportedGVKs gvkList
 
 	changeType ChangeType
 }
 
 func newChangeTrackingUpdater(
-	extractGVK extractGVKFunc,
+	extractGVK kinds.MustExtractGVK,
 	objectTypeCfgs []changeTrackingUpdaterObjectTypeCfg,
 ) *changeTrackingUpdater {
 	var (
diff --git a/internal/mode/static/state/validation/validationfakes/fake_generic_validator.go b/internal/mode/static/state/validation/validationfakes/fake_generic_validator.go
index 294b7dc5..faa9ed1b 100644
--- a/internal/mode/static/state/validation/validationfakes/fake_generic_validator.go
+++ b/internal/mode/static/state/validation/validationfakes/fake_generic_validator.go
@@ -41,6 +41,17 @@ type FakeGenericValidator struct {
 	validateNginxDurationReturnsOnCall map[int]struct {
 		result1 error
 	}
+	ValidateNginxSizeStub        func(string) error
+	validateNginxSizeMutex       sync.RWMutex
+	validateNginxSizeArgsForCall []struct {
+		arg1 string
+	}
+	validateNginxSizeReturns struct {
+		result1 error
+	}
+	validateNginxSizeReturnsOnCall map[int]struct {
+		result1 error
+	}
 	ValidateServiceNameStub        func(string) error
 	validateServiceNameMutex       sync.RWMutex
 	validateServiceNameArgsForCall []struct {
@@ -239,6 +250,67 @@ func (fake *FakeGenericValidator) ValidateNginxDurationReturnsOnCall(i int, resu
 	}{result1}
 }
 
+func (fake *FakeGenericValidator) ValidateNginxSize(arg1 string) error {
+	fake.validateNginxSizeMutex.Lock()
+	ret, specificReturn := fake.validateNginxSizeReturnsOnCall[len(fake.validateNginxSizeArgsForCall)]
+	fake.validateNginxSizeArgsForCall = append(fake.validateNginxSizeArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.ValidateNginxSizeStub
+	fakeReturns := fake.validateNginxSizeReturns
+	fake.recordInvocation("ValidateNginxSize", []interface{}{arg1})
+	fake.validateNginxSizeMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeGenericValidator) ValidateNginxSizeCallCount() int {
+	fake.validateNginxSizeMutex.RLock()
+	defer fake.validateNginxSizeMutex.RUnlock()
+	return len(fake.validateNginxSizeArgsForCall)
+}
+
+func (fake *FakeGenericValidator) ValidateNginxSizeCalls(stub func(string) error) {
+	fake.validateNginxSizeMutex.Lock()
+	defer fake.validateNginxSizeMutex.Unlock()
+	fake.ValidateNginxSizeStub = stub
+}
+
+func (fake *FakeGenericValidator) ValidateNginxSizeArgsForCall(i int) string {
+	fake.validateNginxSizeMutex.RLock()
+	defer fake.validateNginxSizeMutex.RUnlock()
+	argsForCall := fake.validateNginxSizeArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeGenericValidator) ValidateNginxSizeReturns(result1 error) {
+	fake.validateNginxSizeMutex.Lock()
+	defer fake.validateNginxSizeMutex.Unlock()
+	fake.ValidateNginxSizeStub = nil
+	fake.validateNginxSizeReturns = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeGenericValidator) ValidateNginxSizeReturnsOnCall(i int, result1 error) {
+	fake.validateNginxSizeMutex.Lock()
+	defer fake.validateNginxSizeMutex.Unlock()
+	fake.ValidateNginxSizeStub = nil
+	if fake.validateNginxSizeReturnsOnCall == nil {
+		fake.validateNginxSizeReturnsOnCall = make(map[int]struct {
+			result1 error
+		})
+	}
+	fake.validateNginxSizeReturnsOnCall[i] = struct {
+		result1 error
+	}{result1}
+}
+
 func (fake *FakeGenericValidator) ValidateServiceName(arg1 string) error {
 	fake.validateServiceNameMutex.Lock()
 	ret, specificReturn := fake.validateServiceNameReturnsOnCall[len(fake.validateServiceNameArgsForCall)]
@@ -309,6 +381,8 @@ func (fake *FakeGenericValidator) Invocations() map[string][][]interface{} {
 	defer fake.validateEscapedStringNoVarExpansionMutex.RUnlock()
 	fake.validateNginxDurationMutex.RLock()
 	defer fake.validateNginxDurationMutex.RUnlock()
+	fake.validateNginxSizeMutex.RLock()
+	defer fake.validateNginxSizeMutex.RUnlock()
 	fake.validateServiceNameMutex.RLock()
 	defer fake.validateServiceNameMutex.RUnlock()
 	copiedInvocations := map[string][][]interface{}{}
diff --git a/internal/mode/static/state/validation/validationfakes/fake_policy_validator.go b/internal/mode/static/state/validation/validationfakes/fake_policy_validator.go
new file mode 100644
index 00000000..9d08181f
--- /dev/null
+++ b/internal/mode/static/state/validation/validationfakes/fake_policy_validator.go
@@ -0,0 +1,188 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package validationfakes
+
+import (
+	"sync"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
+)
+
+type FakePolicyValidator struct {
+	ConflictsStub        func(policies.Policy, policies.Policy) bool
+	conflictsMutex       sync.RWMutex
+	conflictsArgsForCall []struct {
+		arg1 policies.Policy
+		arg2 policies.Policy
+	}
+	conflictsReturns struct {
+		result1 bool
+	}
+	conflictsReturnsOnCall map[int]struct {
+		result1 bool
+	}
+	ValidateStub        func(policies.Policy) error
+	validateMutex       sync.RWMutex
+	validateArgsForCall []struct {
+		arg1 policies.Policy
+	}
+	validateReturns struct {
+		result1 error
+	}
+	validateReturnsOnCall map[int]struct {
+		result1 error
+	}
+	invocations      map[string][][]interface{}
+	invocationsMutex sync.RWMutex
+}
+
+func (fake *FakePolicyValidator) Conflicts(arg1 policies.Policy, arg2 policies.Policy) bool {
+	fake.conflictsMutex.Lock()
+	ret, specificReturn := fake.conflictsReturnsOnCall[len(fake.conflictsArgsForCall)]
+	fake.conflictsArgsForCall = append(fake.conflictsArgsForCall, struct {
+		arg1 policies.Policy
+		arg2 policies.Policy
+	}{arg1, arg2})
+	stub := fake.ConflictsStub
+	fakeReturns := fake.conflictsReturns
+	fake.recordInvocation("Conflicts", []interface{}{arg1, arg2})
+	fake.conflictsMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicyValidator) ConflictsCallCount() int {
+	fake.conflictsMutex.RLock()
+	defer fake.conflictsMutex.RUnlock()
+	return len(fake.conflictsArgsForCall)
+}
+
+func (fake *FakePolicyValidator) ConflictsCalls(stub func(policies.Policy, policies.Policy) bool) {
+	fake.conflictsMutex.Lock()
+	defer fake.conflictsMutex.Unlock()
+	fake.ConflictsStub = stub
+}
+
+func (fake *FakePolicyValidator) ConflictsArgsForCall(i int) (policies.Policy, policies.Policy) {
+	fake.conflictsMutex.RLock()
+	defer fake.conflictsMutex.RUnlock()
+	argsForCall := fake.conflictsArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakePolicyValidator) ConflictsReturns(result1 bool) {
+	fake.conflictsMutex.Lock()
+	defer fake.conflictsMutex.Unlock()
+	fake.ConflictsStub = nil
+	fake.conflictsReturns = struct {
+		result1 bool
+	}{result1}
+}
+
+func (fake *FakePolicyValidator) ConflictsReturnsOnCall(i int, result1 bool) {
+	fake.conflictsMutex.Lock()
+	defer fake.conflictsMutex.Unlock()
+	fake.ConflictsStub = nil
+	if fake.conflictsReturnsOnCall == nil {
+		fake.conflictsReturnsOnCall = make(map[int]struct {
+			result1 bool
+		})
+	}
+	fake.conflictsReturnsOnCall[i] = struct {
+		result1 bool
+	}{result1}
+}
+
+func (fake *FakePolicyValidator) Validate(arg1 policies.Policy) error {
+	fake.validateMutex.Lock()
+	ret, specificReturn := fake.validateReturnsOnCall[len(fake.validateArgsForCall)]
+	fake.validateArgsForCall = append(fake.validateArgsForCall, struct {
+		arg1 policies.Policy
+	}{arg1})
+	stub := fake.ValidateStub
+	fakeReturns := fake.validateReturns
+	fake.recordInvocation("Validate", []interface{}{arg1})
+	fake.validateMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakePolicyValidator) ValidateCallCount() int {
+	fake.validateMutex.RLock()
+	defer fake.validateMutex.RUnlock()
+	return len(fake.validateArgsForCall)
+}
+
+func (fake *FakePolicyValidator) ValidateCalls(stub func(policies.Policy) error) {
+	fake.validateMutex.Lock()
+	defer fake.validateMutex.Unlock()
+	fake.ValidateStub = stub
+}
+
+func (fake *FakePolicyValidator) ValidateArgsForCall(i int) policies.Policy {
+	fake.validateMutex.RLock()
+	defer fake.validateMutex.RUnlock()
+	argsForCall := fake.validateArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakePolicyValidator) ValidateReturns(result1 error) {
+	fake.validateMutex.Lock()
+	defer fake.validateMutex.Unlock()
+	fake.ValidateStub = nil
+	fake.validateReturns = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakePolicyValidator) ValidateReturnsOnCall(i int, result1 error) {
+	fake.validateMutex.Lock()
+	defer fake.validateMutex.Unlock()
+	fake.ValidateStub = nil
+	if fake.validateReturnsOnCall == nil {
+		fake.validateReturnsOnCall = make(map[int]struct {
+			result1 error
+		})
+	}
+	fake.validateReturnsOnCall[i] = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakePolicyValidator) Invocations() map[string][][]interface{} {
+	fake.invocationsMutex.RLock()
+	defer fake.invocationsMutex.RUnlock()
+	fake.conflictsMutex.RLock()
+	defer fake.conflictsMutex.RUnlock()
+	fake.validateMutex.RLock()
+	defer fake.validateMutex.RUnlock()
+	copiedInvocations := map[string][][]interface{}{}
+	for key, value := range fake.invocations {
+		copiedInvocations[key] = value
+	}
+	return copiedInvocations
+}
+
+func (fake *FakePolicyValidator) recordInvocation(key string, args []interface{}) {
+	fake.invocationsMutex.Lock()
+	defer fake.invocationsMutex.Unlock()
+	if fake.invocations == nil {
+		fake.invocations = map[string][][]interface{}{}
+	}
+	if fake.invocations[key] == nil {
+		fake.invocations[key] = [][]interface{}{}
+	}
+	fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ validation.PolicyValidator = new(FakePolicyValidator)
diff --git a/internal/mode/static/state/validation/validator.go b/internal/mode/static/state/validation/validator.go
index 6f6dd12b..15566901 100644
--- a/internal/mode/static/state/validation/validator.go
+++ b/internal/mode/static/state/validation/validator.go
@@ -2,6 +2,10 @@ package validation
 
 //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
 
+import (
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+)
+
 // Validators include validators for API resources from the perspective of a data-plane.
 // It is used for fields that propagate into the data plane configuration. For example, the path in a routing rule.
 // However, not all such fields are validated: NGF will not validate a field using Validators if it is confident that
@@ -9,6 +13,7 @@ package validation
 type Validators struct {
 	HTTPFieldsValidator HTTPFieldsValidator
 	GenericValidator    GenericValidator
+	PolicyValidator     PolicyValidator
 }
 
 // HTTPFieldsValidator validates the HTTP-related fields of Gateway API resources from the perspective of
@@ -39,5 +44,16 @@ type GenericValidator interface {
 	ValidateEscapedStringNoVarExpansion(value string) error
 	ValidateServiceName(name string) error
 	ValidateNginxDuration(duration string) error
+	ValidateNginxSize(size string) error
 	ValidateEndpoint(endpoint string) error
 }
+
+// PolicyValidator validates an NGF Policy.
+//
+//counterfeiter:generate . PolicyValidator
+type PolicyValidator interface {
+	// Validate validates an NGF Policy.
+	Validate(policy policies.Policy) error
+	// Conflicts returns true if the two Policies conflict.
+	Conflicts(a, b policies.Policy) bool
+}
diff --git a/internal/mode/static/status/prepare_requests.go b/internal/mode/static/status/prepare_requests.go
index 459be5f8..8b979943 100644
--- a/internal/mode/static/status/prepare_requests.go
+++ b/internal/mode/static/status/prepare_requests.go
@@ -13,6 +13,7 @@ import (
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	frameworkStatus "github.com/nginxinc/nginx-gateway-fabric/internal/framework/status"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
@@ -297,6 +298,51 @@ func prepareGatewayRequest(
 	}
 }
 
+func PrepareNGFPolicyRequests(
+	policies map[graph.PolicyKey]*graph.Policy,
+	transitionTime metav1.Time,
+	gatewayCtlrName string,
+) []frameworkStatus.UpdateRequest {
+	reqs := make([]frameworkStatus.UpdateRequest, 0, len(policies))
+
+	for key, pol := range policies {
+		ancestorStatuses := make([]v1alpha2.PolicyAncestorStatus, 0, 1)
+		ancestor := pol.Ancestor
+
+		if ancestor == nil {
+			continue
+		}
+
+		allConds := make([]conditions.Condition, 0, len(pol.Conditions)+len(ancestor.Conditions)+1)
+
+		// The order of conditions matters here.
+		// We add the default condition first, followed by the ancestor conditions, and finally the policy conditions.
+		// DeduplicateConditions will ensure the last condition wins.
+		allConds = append(allConds, staticConds.NewPolicyAccepted())
+		allConds = append(allConds, ancestor.Conditions...)
+		allConds = append(allConds, pol.Conditions...)
+
+		conds := conditions.DeduplicateConditions(allConds)
+		apiConds := conditions.ConvertConditions(conds, pol.Source.GetGeneration(), transitionTime)
+
+		ancestorStatuses = append(ancestorStatuses, v1alpha2.PolicyAncestorStatus{
+			AncestorRef:    ancestor.Ancestor,
+			ControllerName: v1alpha2.GatewayController(gatewayCtlrName),
+			Conditions:     apiConds,
+		})
+
+		status := v1alpha2.PolicyStatus{Ancestors: ancestorStatuses}
+
+		reqs = append(reqs, frameworkStatus.UpdateRequest{
+			NsName:       key.NsName,
+			ResourceType: pol.Source,
+			Setter:       newNGFPolicyStatusSetter(status, gatewayCtlrName),
+		})
+	}
+
+	return reqs
+}
+
 // PrepareBackendTLSPolicyRequests prepares status UpdateRequests for the given BackendTLSPolicies.
 func PrepareBackendTLSPolicyRequests(
 	policies map[types.NamespacedName]*graph.BackendTLSPolicy,
@@ -319,6 +365,8 @@ func PrepareBackendTLSPolicyRequests(
 					AncestorRef: v1.ParentReference{
 						Namespace: (*v1.Namespace)(&pol.Gateway.Namespace),
 						Name:      v1alpha2.ObjectName(pol.Gateway.Name),
+						Group:     helpers.GetPointer[v1.Group](v1.GroupName),
+						Kind:      helpers.GetPointer[v1.Kind](kinds.Gateway),
 					},
 					ControllerName: v1alpha2.GatewayController(gatewayCtlrName),
 					Conditions:     apiConds,
@@ -355,7 +403,9 @@ func PrepareNginxGatewayStatus(
 	var conds []conditions.Condition
 	if cpUpdateRes.Error != nil {
 		msg := "Failed to update control plane configuration"
-		conds = []conditions.Condition{staticConds.NewNginxGatewayInvalid(fmt.Sprintf("%s: %v", msg, cpUpdateRes.Error))}
+		conds = []conditions.Condition{
+			staticConds.NewNginxGatewayInvalid(fmt.Sprintf("%s: %v", msg, cpUpdateRes.Error)),
+		}
 	} else {
 		conds = []conditions.Condition{staticConds.NewNginxGatewayValid()}
 	}
diff --git a/internal/mode/static/status/status_setters.go b/internal/mode/static/status/status_setters.go
index 12cf21cb..141a1a34 100644
--- a/internal/mode/static/status/status_setters.go
+++ b/internal/mode/static/status/status_setters.go
@@ -6,11 +6,13 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+	gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 	"sigs.k8s.io/gateway-api/apis/v1alpha3"
 
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 	frameworkStatus "github.com/nginxinc/nginx-gateway-fabric/internal/framework/status"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
 )
 
 func newNginxGatewayStatusSetter(status ngfAPI.NginxGatewayStatus) frameworkStatus.Setter {
@@ -41,7 +43,7 @@ func newGatewayStatusSetter(status gatewayv1.GatewayStatus) frameworkStatus.Sett
 
 func gwStatusEqual(prev, cur gatewayv1.GatewayStatus) bool {
 	addressesEqual := slices.EqualFunc(prev.Addresses, cur.Addresses, func(a1, a2 gatewayv1.GatewayStatusAddress) bool {
-		if !equalPointers[gatewayv1.AddressType](a1.Type, a2.Type) {
+		if !helpers.EqualPointers[gatewayv1.AddressType](a1.Type, a2.Type) {
 			return false
 		}
 
@@ -74,7 +76,7 @@ func gwStatusEqual(prev, cur gatewayv1.GatewayStatus) bool {
 				return false
 			}
 
-			return equalPointers(k1.Group, k2.Group)
+			return helpers.EqualPointers(k1.Group, k2.Group)
 		})
 	})
 }
@@ -164,11 +166,11 @@ func routeParentStatusEqual(p1, p2 gatewayv1.RouteParentStatus) bool {
 		return false
 	}
 
-	if !equalPointers(p1.ParentRef.Namespace, p2.ParentRef.Namespace) {
+	if !helpers.EqualPointers(p1.ParentRef.Namespace, p2.ParentRef.Namespace) {
 		return false
 	}
 
-	if !equalPointers(p1.ParentRef.SectionName, p2.ParentRef.SectionName) {
+	if !helpers.EqualPointers(p1.ParentRef.SectionName, p2.ParentRef.SectionName) {
 		return false
 	}
 
@@ -212,7 +214,7 @@ func newBackendTLSPolicyStatusSetter(
 		ancestors = append(ancestors, status.Ancestors...)
 		status.Ancestors = ancestors
 
-		if btpStatusEqual(gatewayCtlrName, btp.Status, status) {
+		if policyStatusEqual(gatewayCtlrName, btp.Status, status) {
 			return false
 		}
 
@@ -221,8 +223,40 @@ func newBackendTLSPolicyStatusSetter(
 	}
 }
 
-func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) bool {
-	// Since other controllers may update BackendTLSPolicy status we can't assume anything about the order of the
+func newNGFPolicyStatusSetter(
+	status gatewayv1alpha2.PolicyStatus,
+	gatewayCtlrName string,
+) frameworkStatus.Setter {
+	return func(object client.Object) (wasSet bool) {
+		policy := helpers.MustCastObject[policies.Policy](object)
+		prevStatus := policy.GetPolicyStatus()
+
+		// maxAncestors is the max number of ancestor statuses which is the sum of all new ancestor statuses and all old
+		// ancestor statuses.
+		maxAncestors := len(status.Ancestors) + len(prevStatus.Ancestors)
+		ancestors := make([]gatewayv1alpha2.PolicyAncestorStatus, 0, maxAncestors)
+
+		// keep all the ancestor statuses that belong to other controllers
+		for _, as := range prevStatus.Ancestors {
+			if string(as.ControllerName) != gatewayCtlrName {
+				ancestors = append(ancestors, as)
+			}
+		}
+
+		ancestors = append(ancestors, status.Ancestors...)
+		status.Ancestors = ancestors
+
+		if policyStatusEqual(gatewayCtlrName, prevStatus, status) {
+			return false
+		}
+
+		policy.SetPolicyStatus(status)
+		return true
+	}
+}
+
+func policyStatusEqual(gatewayCtlrName string, prev, cur gatewayv1alpha2.PolicyStatus) bool {
+	// Since other controllers may update Policy status we can't assume anything about the order of the
 	// statuses, and we have to ignore statuses written by other controllers when checking for equality.
 	// Therefore, we can't use slices.EqualFunc here because it cares about the order.
 
@@ -233,7 +267,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) boo
 		}
 
 		exists := slices.ContainsFunc(cur.Ancestors, func(curAncestor v1alpha2.PolicyAncestorStatus) bool {
-			return btpAncestorStatusEqual(prevAncestor, curAncestor)
+			return ancestorStatusEqual(prevAncestor, curAncestor)
 		})
 
 		if !exists {
@@ -244,7 +278,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) boo
 	// Then, we check if the cur status has any PolicyAncestorStatuses that are no longer present in the prev status.
 	for _, curParent := range cur.Ancestors {
 		exists := slices.ContainsFunc(prev.Ancestors, func(prevAncestor v1alpha2.PolicyAncestorStatus) bool {
-			return btpAncestorStatusEqual(curParent, prevAncestor)
+			return ancestorStatusEqual(curParent, prevAncestor)
 		})
 
 		if !exists {
@@ -255,7 +289,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) boo
 	return true
 }
 
-func btpAncestorStatusEqual(p1, p2 v1alpha2.PolicyAncestorStatus) bool {
+func ancestorStatusEqual(p1, p2 v1alpha2.PolicyAncestorStatus) bool {
 	if p1.ControllerName != p2.ControllerName {
 		return false
 	}
@@ -264,34 +298,18 @@ func btpAncestorStatusEqual(p1, p2 v1alpha2.PolicyAncestorStatus) bool {
 		return false
 	}
 
-	if !equalPointers(p1.AncestorRef.Namespace, p2.AncestorRef.Namespace) {
+	if !helpers.EqualPointers(p1.AncestorRef.Namespace, p2.AncestorRef.Namespace) {
 		return false
 	}
 
-	// we ignore the rest of the AncestorRef fields because we do not set them
-
-	return frameworkStatus.ConditionsEqual(p1.Conditions, p2.Conditions)
-}
-
-// equalPointers returns whether two pointers are equal.
-// Pointers are considered equal if one of the following is true:
-// - They are both nil.
-// - One is nil and the other is empty (e.g. nil string and "").
-// - They are both non-nil, and their values are the same.
-func equalPointers[T comparable](p1, p2 *T) bool {
-	if p1 == nil && p2 == nil {
-		return true
-	}
-
-	var p1Val, p2Val T
-
-	if p1 != nil {
-		p1Val = *p1
+	if !helpers.EqualPointers(p1.AncestorRef.Group, p2.AncestorRef.Group) {
+		return false
 	}
 
-	if p2 != nil {
-		p2Val = *p2
+	if !helpers.EqualPointers(p1.AncestorRef.Kind, p2.AncestorRef.Kind) {
+		return false
 	}
+	// we ignore the rest of the AncestorRef fields because we do not set them
 
-	return p1Val == p2Val
+	return frameworkStatus.ConditionsEqual(p1.Conditions, p2.Conditions)
 }



Test patch:
diff --git a/internal/framework/controller/register_test.go b/internal/framework/controller/register_test.go
index d038e44b..3f6f01fe 100644
--- a/internal/framework/controller/register_test.go
+++ b/internal/framework/controller/register_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/controllerfakes"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/predicate"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 )
 
 func TestRegister(t *testing.T) {
@@ -54,7 +55,7 @@ func TestRegister(t *testing.T) {
 
 	objectTypeWithGVK := &v1.HTTPRoute{}
 	objectTypeWithGVK.SetGroupVersionKind(
-		schema.GroupVersionKind{Group: v1.GroupName, Version: "v1", Kind: "HTTPRoute"},
+		schema.GroupVersionKind{Group: v1.GroupName, Version: "v1", Kind: kinds.HTTPRoute},
 	)
 
 	objectTypeNoGVK := &v1.HTTPRoute{}
diff --git a/internal/framework/helpers/helpers_test.go b/internal/framework/helpers/helpers_test.go
index c9838329..fbd1498b 100644
--- a/internal/framework/helpers/helpers_test.go
+++ b/internal/framework/helpers/helpers_test.go
@@ -1,13 +1,15 @@
-package helpers
+package helpers_test
 
 import (
 	"testing"
+	"text/template"
 
 	. "github.com/onsi/gomega"
-
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 	gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
 )
 
 func TestMustCastObject(t *testing.T) {
@@ -16,10 +18,89 @@ func TestMustCastObject(t *testing.T) {
 	var obj client.Object = &gatewayv1.Gateway{}
 
 	g.Expect(func() {
-		_ = MustCastObject[*gatewayv1.Gateway](obj)
+		_ = helpers.MustCastObject[*gatewayv1.Gateway](obj)
 	}).ToNot(Panic())
 
 	g.Expect(func() {
-		_ = MustCastObject[*gatewayv1alpha3.BackendTLSPolicy](obj)
+		_ = helpers.MustCastObject[*gatewayv1alpha3.BackendTLSPolicy](obj)
 	}).To(Panic())
 }
+
+func TestEqualPointers(t *testing.T) {
+	tests := []struct {
+		p1       *string
+		p2       *string
+		name     string
+		expEqual bool
+	}{
+		{
+			name:     "first pointer nil; second has non-empty value",
+			p1:       nil,
+			p2:       helpers.GetPointer("test"),
+			expEqual: false,
+		},
+		{
+			name:     "second pointer nil; first has non-empty value",
+			p1:       helpers.GetPointer("test"),
+			p2:       nil,
+			expEqual: false,
+		},
+		{
+			name:     "different values",
+			p1:       helpers.GetPointer("test"),
+			p2:       helpers.GetPointer("different"),
+			expEqual: false,
+		},
+		{
+			name:     "both pointers nil",
+			p1:       nil,
+			p2:       nil,
+			expEqual: true,
+		},
+		{
+			name:     "first pointer nil; second empty",
+			p1:       nil,
+			p2:       helpers.GetPointer(""),
+			expEqual: true,
+		},
+		{
+			name:     "second pointer nil; first empty",
+			p1:       helpers.GetPointer(""),
+			p2:       nil,
+			expEqual: true,
+		},
+		{
+			name:     "same value",
+			p1:       helpers.GetPointer("test"),
+			p2:       helpers.GetPointer("test"),
+			expEqual: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			val := helpers.EqualPointers(test.p1, test.p2)
+			g.Expect(val).To(Equal(test.expEqual))
+		})
+	}
+}
+
+func TestMustExecuteTemplate(t *testing.T) {
+	g := NewWithT(t)
+
+	tmpl := template.Must(template.New("test").Parse(`Hello {{.}}`))
+	bytes := helpers.MustExecuteTemplate(tmpl, "you")
+	g.Expect(string(bytes)).To(Equal("Hello you"))
+}
+
+func TestMustExecuteTemplatePanics(t *testing.T) {
+	g := NewWithT(t)
+
+	execute := func() {
+		helpers.MustExecuteTemplate(nil, nil)
+	}
+
+	g.Expect(execute).To(Panic())
+}
diff --git a/internal/framework/status/updater_test.go b/internal/framework/status/updater_test.go
index 7c047e42..1b8c944e 100644
--- a/internal/framework/status/updater_test.go
+++ b/internal/framework/status/updater_test.go
@@ -14,6 +14,7 @@ import (
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 )
 
 func createGC(name string) *v1.GatewayClass {
@@ -22,7 +23,7 @@ func createGC(name string) *v1.GatewayClass {
 			Name: name,
 		},
 		TypeMeta: metav1.TypeMeta{
-			Kind:       "GatewayClass",
+			Kind:       kinds.GatewayClass,
 			APIVersion: "gateway.networking.k8s.io/v1",
 		},
 	}
diff --git a/internal/mode/static/manager_test.go b/internal/mode/static/manager_test.go
index dbcfc1d2..1d02cee6 100644
--- a/internal/mode/static/manager_test.go
+++ b/internal/mode/static/manager_test.go
@@ -56,6 +56,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
 				&ngfAPI.NginxProxyList{},
 				&gatewayv1.GRPCRouteList{},
 				partialObjectMetadataList,
+				&ngfAPI.ClientSettingsPolicyList{},
 			},
 		},
 		{
@@ -78,6 +79,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
 				&ngfAPI.NginxProxyList{},
 				&gatewayv1.GRPCRouteList{},
 				partialObjectMetadataList,
+				&ngfAPI.ClientSettingsPolicyList{},
 			},
 		},
 		{
@@ -99,9 +101,10 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
 				&gatewayv1.HTTPRouteList{},
 				&gatewayv1beta1.ReferenceGrantList{},
 				&ngfAPI.NginxProxyList{},
-				&gatewayv1.GRPCRouteList{},
 				partialObjectMetadataList,
 				&gatewayv1alpha3.BackendTLSPolicyList{},
+				&gatewayv1.GRPCRouteList{},
+				&ngfAPI.ClientSettingsPolicyList{},
 			},
 			experimentalEnabled: true,
 		},
diff --git a/internal/mode/static/nginx/config/execute_test.go b/internal/mode/static/nginx/config/execute_test.go
deleted file mode 100644
index 61538973..00000000
--- a/internal/mode/static/nginx/config/execute_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package config
-
-import (
-	"testing"
-
-	. "github.com/onsi/gomega"
-
-	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
-)
-
-func TestExecute(t *testing.T) {
-	g := NewWithT(t)
-	defer func() {
-		g.Expect(recover()).Should(BeNil())
-	}()
-	bytes := execute(serversTemplate, []http.Server{})
-	g.Expect(bytes).ToNot(BeEmpty())
-}
-
-func TestExecutePanics(t *testing.T) {
-	defer func() {
-		g := NewWithT(t)
-		g.Expect(recover()).ToNot(BeNil())
-	}()
-
-	_ = execute(serversTemplate, "not-correct-data")
-}
diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go
index e3d57bc2..ae458e8f 100644
--- a/internal/mode/static/nginx/config/servers_test.go
+++ b/internal/mode/static/nginx/config/servers_test.go
@@ -28,6 +28,12 @@ func TestExecuteServers(t *testing.T) {
 			{
 				Hostname: "cafe.example.com",
 				Port:     8080,
+				Additions: []dataplane.Addition{
+					{
+						Bytes:      []byte("addition-1"),
+						Identifier: "addition-1",
+					},
+				},
 			},
 		},
 		SSLServers: []dataplane.VirtualServer{
@@ -74,6 +80,16 @@ func TestExecuteServers(t *testing.T) {
 						},
 					},
 				},
+				Additions: []dataplane.Addition{
+					{
+						Bytes:      []byte("addition-1"),
+						Identifier: "addition-1", // duplicate
+					},
+					{
+						Bytes:      []byte("addition-2"),
+						Identifier: "addition-2",
+					},
+				},
 			},
 		},
 	}
@@ -89,14 +105,35 @@ func TestExecuteServers(t *testing.T) {
 		"ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 2,
 		"proxy_ssl_server_name on;":                                1,
 	}
+
+	type assertion func(g *WithT, data string)
+
+	expectedResults := map[string]assertion{
+		httpConfigFile: func(g *WithT, data string) {
+			for expSubStr, expCount := range expSubStrings {
+				g.Expect(strings.Count(data, expSubStr)).To(Equal(expCount))
+			}
+		},
+		httpMatchVarsFile: func(g *WithT, data string) {
+			g.Expect(data).To(Equal("{}"))
+		},
+		includesFolder + "/addition-1.conf": func(g *WithT, data string) {
+			g.Expect(data).To(Equal("addition-1"))
+		},
+		includesFolder + "/addition-2.conf": func(g *WithT, data string) {
+			g.Expect(data).To(Equal("addition-2"))
+		},
+	}
 	g := NewWithT(t)
-	serverResults := executeServers(conf)
-	g.Expect(serverResults).To(HaveLen(2))
-	serverConf := string(serverResults[0].data)
-	httpMatchConf := string(serverResults[1].data)
-	g.Expect(httpMatchConf).To(Equal("{}"))
-	for expSubStr, expCount := range expSubStrings {
-		g.Expect(strings.Count(serverConf, expSubStr)).To(Equal(expCount))
+
+	results := executeServers(conf)
+	g.Expect(results).To(HaveLen(len(expectedResults)))
+
+	for _, res := range results {
+		g.Expect(expectedResults).To(HaveKey(res.dest), "executeServers returned unexpected result destination")
+
+		assertData := expectedResults[res.dest]
+		assertData(g, string(res.data))
 	}
 }
 
@@ -546,6 +583,40 @@ func TestCreateServers(t *testing.T) {
 			},
 			GRPC: true,
 		},
+		{
+			Path:     "/addition-path-only-match",
+			PathType: dataplane.PathTypeExact,
+			MatchRules: []dataplane.MatchRule{
+				{
+					Match:        dataplane.Match{},
+					BackendGroup: fooGroup,
+					Additions: []dataplane.Addition{
+						{
+							Bytes:      []byte("path-only-match-addition"),
+							Identifier: "path-only-match-addition",
+						},
+					},
+				},
+			},
+		},
+		{
+			Path:     "/addition-header-match",
+			PathType: dataplane.PathTypeExact,
+			MatchRules: []dataplane.MatchRule{
+				{
+					Match: dataplane.Match{
+						Method: helpers.GetPointer("GET"),
+					},
+					BackendGroup: fooGroup,
+					Additions: []dataplane.Addition{
+						{
+							Bytes:      []byte("match-addition"),
+							Identifier: "match-addition",
+						},
+					},
+				},
+			},
+		},
 	}
 
 	httpServers := []dataplane.VirtualServer{
@@ -557,6 +628,16 @@ func TestCreateServers(t *testing.T) {
 			Hostname:  "cafe.example.com",
 			PathRules: cafePathRules,
 			Port:      8080,
+			Additions: []dataplane.Addition{
+				{
+					Bytes:      []byte("server-addition-1"),
+					Identifier: "server-addition-1",
+				},
+				{
+					Bytes:      []byte("server-addition-2"),
+					Identifier: "server-addition-2",
+				},
+			},
 		},
 	}
 
@@ -570,6 +651,16 @@ func TestCreateServers(t *testing.T) {
 			SSL:       &dataplane.SSL{KeyPairID: sslKeyPairID},
 			PathRules: cafePathRules,
 			Port:      8443,
+			Additions: []dataplane.Addition{
+				{
+					Bytes:      []byte("server-addition-1"),
+					Identifier: "server-addition-1",
+				},
+				{
+					Bytes:      []byte("server-addition-3"),
+					Identifier: "server-addition-3",
+				},
+			},
 		},
 	}
 
@@ -611,6 +702,12 @@ func TestCreateServers(t *testing.T) {
 				Any:          false,
 			},
 		},
+		"1_17": {
+			{
+				Method:       "GET",
+				RedirectPath: "@rule17-route0",
+			},
+		},
 	}
 
 	allExpMatchPair := make(httpMatchPairs)
@@ -899,6 +996,26 @@ func TestCreateServers(t *testing.T) {
 				GRPC:            true,
 				ProxySetHeaders: grpcBaseHeaders,
 			},
+			{
+				Path:            "= /addition-path-only-match",
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: httpBaseHeaders,
+				Includes: []string{
+					includesFolder + "/path-only-match-addition.conf",
+				},
+			},
+			{
+				Path:            "@rule17-route0",
+				ProxyPass:       "http://test_foo_80$request_uri",
+				ProxySetHeaders: httpBaseHeaders,
+				Includes: []string{
+					includesFolder + "/match-addition.conf",
+				},
+			},
+			{
+				Path:         "= /addition-header-match",
+				HTTPMatchKey: ssl + "1_17",
+			},
 		}
 	}
 
@@ -914,6 +1031,10 @@ func TestCreateServers(t *testing.T) {
 			Locations:  getExpectedLocations(false),
 			Port:       8080,
 			GRPC:       true,
+			Includes: []string{
+				includesFolder + "/server-addition-1.conf",
+				includesFolder + "/server-addition-2.conf",
+			},
 		},
 		{
 			IsDefaultSSL: true,
@@ -928,6 +1049,10 @@ func TestCreateServers(t *testing.T) {
 			Locations: getExpectedLocations(true),
 			Port:      8443,
 			GRPC:      true,
+			Includes: []string{
+				includesFolder + "/server-addition-1.conf",
+				includesFolder + "/server-addition-3.conf",
+			},
 		},
 	}
 
@@ -2220,3 +2345,173 @@ func TestGenerateResponseHeaders(t *testing.T) {
 		})
 	}
 }
+
+func TestCreateIncludes(t *testing.T) {
+	tests := []struct {
+		name      string
+		additions []dataplane.Addition
+		includes  []string
+	}{
+		{
+			name:      "no additions",
+			additions: nil,
+			includes:  nil,
+		},
+		{
+			name: "additions",
+			additions: []dataplane.Addition{
+				{
+					Bytes:      []byte("one"),
+					Identifier: "one",
+				},
+				{
+					Bytes:      []byte("two"),
+					Identifier: "two",
+				},
+				{
+					Bytes:      []byte("three"),
+					Identifier: "three",
+				},
+			},
+			includes: []string{
+				includesFolder + "/one.conf",
+				includesFolder + "/two.conf",
+				includesFolder + "/three.conf",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			includes := createIncludes(test.additions)
+			g.Expect(includes).To(Equal(test.includes))
+		})
+	}
+}
+
+func TestCreateAdditionFileResults(t *testing.T) {
+	conf := dataplane.Configuration{
+		HTTPServers: []dataplane.VirtualServer{
+			{
+				Additions: []dataplane.Addition{
+					{
+						Identifier: "include-1",
+						Bytes:      []byte("include-1"),
+					},
+					{
+						Identifier: "include-2",
+						Bytes:      []byte("include-2"),
+					},
+				},
+				PathRules: []dataplane.PathRule{
+					{
+						MatchRules: []dataplane.MatchRule{
+							{
+								Additions: []dataplane.Addition{
+									{
+										Identifier: "include-3",
+										Bytes:      []byte("include-3"),
+									},
+									{
+										Identifier: "include-4",
+										Bytes:      []byte("include-4"),
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			{
+				Additions: []dataplane.Addition{
+					{
+						Identifier: "include-1", // dupe
+						Bytes:      []byte("include-1"),
+					},
+					{
+						Identifier: "include-2", // dupe
+						Bytes:      []byte("include-2"),
+					},
+				},
+			},
+		},
+		SSLServers: []dataplane.VirtualServer{
+			{
+				Additions: []dataplane.Addition{
+					{
+						Identifier: "include-1", // dupe
+						Bytes:      []byte("include-1"),
+					},
+					{
+						Identifier: "include-2", // dupe
+						Bytes:      []byte("include-2"),
+					},
+				},
+				PathRules: []dataplane.PathRule{
+					{
+						MatchRules: []dataplane.MatchRule{
+							{
+								Additions: []dataplane.Addition{
+									{
+										Identifier: "include-3",
+										Bytes:      []byte("include-3"), // dupe
+									},
+									{
+										Identifier: "include-5",
+										Bytes:      []byte("include-5"), // dupe
+									},
+									{
+										Identifier: "include-6",
+										Bytes:      []byte("include-6"),
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	results := createAdditionFileResults(conf)
+
+	expResults := []executeResult{
+		{
+			dest: includesFolder + "/" + "include-1.conf",
+			data: []byte("include-1"),
+		},
+		{
+			dest: includesFolder + "/" + "include-2.conf",
+			data: []byte("include-2"),
+		},
+		{
+			dest: includesFolder + "/" + "include-3.conf",
+			data: []byte("include-3"),
+		},
+		{
+			dest: includesFolder + "/" + "include-4.conf",
+			data: []byte("include-4"),
+		},
+		{
+			dest: includesFolder + "/" + "include-5.conf",
+			data: []byte("include-5"),
+		},
+		{
+			dest: includesFolder + "/" + "include-6.conf",
+			data: []byte("include-6"),
+		},
+	}
+
+	g := NewWithT(t)
+
+	g.Expect(results).To(ConsistOf(expResults))
+}
+
+func TestAdditionFilename(t *testing.T) {
+	g := NewWithT(t)
+
+	name := createAdditionFileName(dataplane.Addition{Identifier: "my-addition"})
+	g.Expect(name).To(Equal(includesFolder + "/" + "my-addition.conf"))
+}
diff --git a/internal/mode/static/nginx/config/validation/generic_test.go b/internal/mode/static/nginx/config/validation/generic_test.go
index b07131cc..86903bfa 100644
--- a/internal/mode/static/nginx/config/validation/generic_test.go
+++ b/internal/mode/static/nginx/config/validation/generic_test.go
@@ -64,6 +64,27 @@ func TestValidateNginxDuration(t *testing.T) {
 	)
 }
 
+func TestValidateNginxSize(t *testing.T) {
+	validator := GenericValidator{}
+
+	testValidValuesForSimpleValidator(
+		t,
+		validator.ValidateNginxSize,
+		`1024`,
+		`10k`,
+		`123m`,
+		`4096g`,
+	)
+
+	testInvalidValuesForSimpleValidator(
+		t,
+		validator.ValidateNginxSize,
+		`test`,
+		`12345`,
+		`5b`,
+	)
+}
+
 func TestValidateEndpoint(t *testing.T) {
 	validator := GenericValidator{}
 
diff --git a/internal/mode/static/policies/clientsettings/generator_test.go b/internal/mode/static/policies/clientsettings/generator_test.go
new file mode 100644
index 00000000..a286342d
--- /dev/null
+++ b/internal/mode/static/policies/clientsettings/generator_test.go
@@ -0,0 +1,173 @@
+package clientsettings_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+
+	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/clientsettings"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes"
+)
+
+func TestGenerate(t *testing.T) {
+	maxSize := helpers.GetPointer[ngfAPI.Size]("10m")
+	bodyTimeout := helpers.GetPointer[ngfAPI.Duration]("600ms")
+	keepaliveRequests := helpers.GetPointer[int32](900)
+	keepaliveTime := helpers.GetPointer[ngfAPI.Duration]("50s")
+	keepaliveServerTimeout := helpers.GetPointer[ngfAPI.Duration]("30s")
+	keepaliveHeaderTimeout := helpers.GetPointer[ngfAPI.Duration]("60s")
+
+	tests := []struct {
+		name       string
+		policy     policies.Policy
+		expStrings []string
+	}{
+		{
+			name: "body max size populated",
+			policy: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					Body: &ngfAPI.ClientBody{
+						MaxSize: maxSize,
+					},
+				},
+			},
+			expStrings: []string{
+				"client_max_body_size 10m;",
+			},
+		},
+		{
+			name: "body timeout populated",
+			policy: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					Body: &ngfAPI.ClientBody{
+						Timeout: bodyTimeout,
+					},
+				},
+			},
+			expStrings: []string{
+				"client_body_timeout 600ms",
+			},
+		},
+		{
+			name: "keepalive requests populated",
+			policy: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Requests: keepaliveRequests,
+					},
+				},
+			},
+			expStrings: []string{
+				"keepalive_requests 900;",
+			},
+		},
+		{
+			name: "keepalive time populated",
+			policy: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Time: keepaliveTime,
+					},
+				},
+			},
+			expStrings: []string{
+				"keepalive_time 50s;",
+			},
+		},
+		{
+			name: "keepalive timeout server populated",
+			policy: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Timeout: &ngfAPI.ClientKeepAliveTimeout{
+							Server: keepaliveServerTimeout,
+						},
+					},
+				},
+			},
+			expStrings: []string{
+				"keepalive_timeout 30s;",
+			},
+		},
+		{
+			name: "keepalive timeout server and header populated",
+			policy: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Timeout: &ngfAPI.ClientKeepAliveTimeout{
+							Server: keepaliveServerTimeout,
+							Header: keepaliveHeaderTimeout,
+						},
+					},
+				},
+			},
+			expStrings: []string{
+				"keepalive_timeout 30s 60s;",
+			},
+		},
+		{
+			name: "keepalive timeout header populated",
+			policy: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Timeout: &ngfAPI.ClientKeepAliveTimeout{
+							Header: keepaliveHeaderTimeout,
+						},
+					},
+				},
+			},
+			expStrings: []string{}, // header timeout is ignored if server timeout is not populated
+		},
+		{
+			name: "all fields populated",
+			policy: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					Body: &ngfAPI.ClientBody{
+						MaxSize: maxSize,
+						Timeout: bodyTimeout,
+					},
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Requests: keepaliveRequests,
+						Time:     keepaliveTime,
+						Timeout: &ngfAPI.ClientKeepAliveTimeout{
+							Server: keepaliveServerTimeout,
+							Header: keepaliveHeaderTimeout,
+						},
+					},
+				},
+			},
+			expStrings: []string{
+				"client_max_body_size 10m;",
+				"client_body_timeout 600ms",
+				"keepalive_requests 900;",
+				"keepalive_time 50s;",
+				"keepalive_timeout 30s 60s;",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			cfgString := string(clientsettings.Generate(test.policy))
+
+			for _, str := range test.expStrings {
+				g.Expect(cfgString).To(ContainSubstring(str))
+			}
+		})
+	}
+}
+
+func TestGeneratePanics(t *testing.T) {
+	g := NewWithT(t)
+
+	generate := func() {
+		clientsettings.Generate(&policiesfakes.FakePolicy{})
+	}
+
+	g.Expect(generate).To(Panic())
+}
diff --git a/internal/mode/static/policies/clientsettings/validator_test.go b/internal/mode/static/policies/clientsettings/validator_test.go
new file mode 100644
index 00000000..04d83ae9
--- /dev/null
+++ b/internal/mode/static/policies/clientsettings/validator_test.go
@@ -0,0 +1,267 @@
+package clientsettings_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/validation"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/clientsettings"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes"
+)
+
+type policyModFunc func(policy *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy
+
+func createValidPolicy() *ngfAPI.ClientSettingsPolicy {
+	return &ngfAPI.ClientSettingsPolicy{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+		},
+		Spec: ngfAPI.ClientSettingsPolicySpec{
+			TargetRef: v1alpha2.LocalPolicyTargetReference{
+				Group: v1.GroupName,
+				Kind:  kinds.Gateway,
+				Name:  "gateway",
+			},
+			Body: &ngfAPI.ClientBody{
+				MaxSize: helpers.GetPointer[ngfAPI.Size]("10m"),
+				Timeout: helpers.GetPointer[ngfAPI.Duration]("600ms"),
+			},
+			KeepAlive: &ngfAPI.ClientKeepAlive{
+				Requests: helpers.GetPointer[int32](900),
+				Time:     helpers.GetPointer[ngfAPI.Duration]("50s"),
+				Timeout: &ngfAPI.ClientKeepAliveTimeout{
+					Server: helpers.GetPointer[ngfAPI.Duration]("30s"),
+					Header: helpers.GetPointer[ngfAPI.Duration]("60s"),
+				},
+			},
+		},
+		Status: v1alpha2.PolicyStatus{},
+	}
+}
+
+func createModifiedPolicy(mod policyModFunc) *ngfAPI.ClientSettingsPolicy {
+	return mod(createValidPolicy())
+}
+
+func TestValidator_Validate(t *testing.T) {
+	tests := []struct {
+		name             string
+		policy           *ngfAPI.ClientSettingsPolicy
+		expErrSubstrings []string
+	}{
+		{
+			name: "invalid target ref; unsupported group",
+			policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy {
+				p.Spec.TargetRef.Group = "Unsupported"
+				return p
+			}),
+			expErrSubstrings: []string{"spec.targetRef.group"},
+		},
+		{
+			name: "invalid target ref; unsupported kind",
+			policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy {
+				p.Spec.TargetRef.Kind = "Unsupported"
+				return p
+			}),
+			expErrSubstrings: []string{"spec.targetRef.kind"},
+		},
+		{
+			name: "invalid client max body size",
+			policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy {
+				p.Spec.Body.MaxSize = helpers.GetPointer[ngfAPI.Size]("invalid")
+				return p
+			}),
+			expErrSubstrings: []string{"spec.body.maxSize"},
+		},
+		{
+			name: "invalid durations",
+			policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy {
+				p.Spec.Body.Timeout = helpers.GetPointer[ngfAPI.Duration]("invalid")
+				p.Spec.KeepAlive.Time = helpers.GetPointer[ngfAPI.Duration]("invalid")
+				p.Spec.KeepAlive.Timeout.Server = helpers.GetPointer[ngfAPI.Duration]("invalid")
+				p.Spec.KeepAlive.Timeout.Header = helpers.GetPointer[ngfAPI.Duration]("invalid")
+				return p
+			}),
+			expErrSubstrings: []string{
+				"spec.body.timeout",
+				"spec.keepAlive.time",
+				"spec.keepAlive.timeout.server",
+				"spec.keepAlive.timeout.header",
+			},
+		},
+		{
+			name: "invalid keepalive timeout; header provided without server",
+			policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy {
+				p.Spec.KeepAlive.Timeout.Server = nil
+				return p
+			}),
+			expErrSubstrings: []string{"spec.keepAlive.timeout"},
+		},
+		{
+			name:             "valid",
+			policy:           createValidPolicy(),
+			expErrSubstrings: nil,
+		},
+	}
+
+	v := clientsettings.NewValidator(validation.GenericValidator{})
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			err := v.Validate(test.policy)
+
+			if len(test.expErrSubstrings) == 0 {
+				g.Expect(err).ToNot(HaveOccurred())
+			} else {
+				g.Expect(err).To(HaveOccurred())
+			}
+
+			for _, str := range test.expErrSubstrings {
+				g.Expect(err.Error()).To(ContainSubstring(str))
+			}
+		})
+	}
+}
+
+func TestValidator_ValidatePanics(t *testing.T) {
+	v := clientsettings.NewValidator(nil)
+
+	validate := func() {
+		_ = v.Validate(&policiesfakes.FakePolicy{})
+	}
+
+	g := NewWithT(t)
+
+	g.Expect(validate).To(Panic())
+}
+
+func TestValidator_Conflicts(t *testing.T) {
+	tests := []struct {
+		polA      *ngfAPI.ClientSettingsPolicy
+		polB      *ngfAPI.ClientSettingsPolicy
+		name      string
+		conflicts bool
+	}{
+		{
+			name: "no conflicts",
+			polA: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					Body: &ngfAPI.ClientBody{
+						MaxSize: helpers.GetPointer[ngfAPI.Size]("10m"),
+					},
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Requests: helpers.GetPointer[int32](900),
+						Time:     helpers.GetPointer[ngfAPI.Duration]("50s"),
+					},
+				},
+			},
+			polB: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					Body: &ngfAPI.ClientBody{
+						Timeout: helpers.GetPointer[ngfAPI.Duration]("600ms"),
+					},
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Timeout: &ngfAPI.ClientKeepAliveTimeout{
+							Server: helpers.GetPointer[ngfAPI.Duration]("30s"),
+							Header: helpers.GetPointer[ngfAPI.Duration]("60s"),
+						},
+					},
+				},
+			},
+			conflicts: false,
+		},
+		{
+			name: "body max size conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					Body: &ngfAPI.ClientBody{
+						MaxSize: helpers.GetPointer[ngfAPI.Size]("10m"),
+					},
+				},
+			},
+			conflicts: true,
+		},
+		{
+			name: "body timeout conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					Body: &ngfAPI.ClientBody{
+						Timeout: helpers.GetPointer[ngfAPI.Duration]("600ms"),
+					},
+				},
+			},
+			conflicts: true,
+		},
+		{
+			name: "keepalive requests conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Requests: helpers.GetPointer[int32](900),
+					},
+				},
+			},
+			conflicts: true,
+		},
+		{
+			name: "keepalive time conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Time: helpers.GetPointer[ngfAPI.Duration]("50s"),
+					},
+				},
+			},
+			conflicts: true,
+		},
+		{
+			name: "keepalive timeout conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.ClientSettingsPolicy{
+				Spec: ngfAPI.ClientSettingsPolicySpec{
+					KeepAlive: &ngfAPI.ClientKeepAlive{
+						Timeout: &ngfAPI.ClientKeepAliveTimeout{
+							Server: helpers.GetPointer[ngfAPI.Duration]("30s"),
+						},
+					},
+				},
+			},
+			conflicts: true,
+		},
+	}
+
+	v := clientsettings.NewValidator(nil)
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			g.Expect(v.Conflicts(test.polA, test.polB)).To(Equal(test.conflicts))
+		})
+	}
+}
+
+func TestValidator_ConflictsPanics(t *testing.T) {
+	v := clientsettings.NewValidator(nil)
+
+	conflicts := func() {
+		_ = v.Conflicts(&policiesfakes.FakePolicy{}, &policiesfakes.FakePolicy{})
+	}
+
+	g := NewWithT(t)
+
+	g.Expect(conflicts).To(Panic())
+}
diff --git a/internal/mode/static/policies/manager_test.go b/internal/mode/static/policies/manager_test.go
new file mode 100644
index 00000000..db6d81fd
--- /dev/null
+++ b/internal/mode/static/policies/manager_test.go
@@ -0,0 +1,115 @@
+package policies_test
+
+import (
+	"errors"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes"
+)
+
+var _ = Describe("Policy Manager", func() {
+	orangeGVK := schema.GroupVersionKind{Group: "fruit", Version: "1", Kind: "orange"}
+	orangePolicy := &policiesfakes.FakePolicy{
+		GetNameStub: func() string {
+			return "orange"
+		},
+	}
+
+	appleGVK := schema.GroupVersionKind{Group: "fruit", Version: "1", Kind: "apple"}
+	applePolicy := &policiesfakes.FakePolicy{
+		GetNameStub: func() string {
+			return "apple"
+		},
+	}
+
+	mustExtractGVK := func(object client.Object) schema.GroupVersionKind {
+		switch object.GetName() {
+		case "apple":
+			return appleGVK
+		case "orange":
+			return orangeGVK
+		default:
+			return schema.GroupVersionKind{}
+		}
+	}
+
+	mgr := policies.NewManager(
+		mustExtractGVK,
+		policies.ManagerConfig{
+			Validator: &policiesfakes.FakeValidator{
+				ValidateStub:  func(_ policies.Policy) error { return errors.New("apple error") },
+				ConflictsStub: func(_ policies.Policy, _ policies.Policy) bool { return true },
+			},
+			Generator: func(_ policies.Policy) []byte {
+				return []byte("apple")
+			},
+			GVK: appleGVK,
+		},
+		policies.ManagerConfig{
+			Validator: &policiesfakes.FakeValidator{
+				ValidateStub:  func(_ policies.Policy) error { return errors.New("orange error") },
+				ConflictsStub: func(_ policies.Policy, _ policies.Policy) bool { return false },
+			},
+			Generator: func(_ policies.Policy) []byte {
+				return []byte("orange")
+			},
+			GVK: orangeGVK,
+		},
+	)
+
+	Context("Validation", func() {
+		When("Policy is registered with manager", func() {
+			It("Validates the policy", func() {
+				err := mgr.Validate(applePolicy)
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(Equal("apple error"))
+
+				err = mgr.Validate(orangePolicy)
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(Equal("orange error"))
+			})
+			It("Returns whether the policies conflict", func() {
+				Expect(mgr.Conflicts(applePolicy, applePolicy)).To(BeTrue())
+				Expect(mgr.Conflicts(orangePolicy, orangePolicy)).To(BeFalse())
+			})
+		})
+		When("Policy is not registered with manager", func() {
+			It("Panics on call to validate", func() {
+				validate := func() {
+					_ = mgr.Validate(&policiesfakes.FakePolicy{})
+				}
+
+				Expect(validate).To(Panic())
+			})
+			It("panics on call to conflicts", func() {
+				conflict := func() {
+					_ = mgr.Conflicts(&policiesfakes.FakePolicy{}, &policiesfakes.FakePolicy{})
+				}
+
+				Expect(conflict).To(Panic())
+			})
+		})
+	})
+	Context("Generation", func() {
+		When("Policy is registered with manager", func() {
+			It("Generates the configuration for the policy", func() {
+				Expect(mgr.Generate(applePolicy)).To(Equal([]byte("apple")))
+				Expect(mgr.Generate(orangePolicy)).To(Equal([]byte("orange")))
+			})
+		})
+		When("Policy is not registered with manager", func() {
+			It("Panics on generate", func() {
+				generate := func() {
+					_ = mgr.Generate(&policiesfakes.FakePolicy{})
+				}
+
+				Expect(generate).To(Panic())
+			})
+		})
+	})
+})
diff --git a/internal/mode/static/policies/policies_suite_test.go b/internal/mode/static/policies/policies_suite_test.go
new file mode 100644
index 00000000..6703df23
--- /dev/null
+++ b/internal/mode/static/policies/policies_suite_test.go
@@ -0,0 +1,13 @@
+package policies_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestPolicies(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Policies Suite")
+}
diff --git a/internal/mode/static/sort/sort_test.go b/internal/mode/static/sort/sort_test.go
index e7cac065..d1b63e83 100644
--- a/internal/mode/static/sort/sort_test.go
+++ b/internal/mode/static/sort/sort_test.go
@@ -6,6 +6,8 @@ import (
 
 	. "github.com/onsi/gomega"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
 )
 
 func TestLessObjectMeta(t *testing.T) {
@@ -27,7 +29,6 @@ func TestLessObjectMeta(t *testing.T) {
 			meta2: &metav1.ObjectMeta{
 				Namespace:         "ns1",
 				Name:              "meta2",
-				UID:               "b",
 				CreationTimestamp: later,
 			},
 			name:     "first is less by timestamp",
@@ -89,3 +90,100 @@ func TestLessObjectMeta(t *testing.T) {
 		})
 	}
 }
+
+func TestLessClientObject(t *testing.T) {
+	before := metav1.Now()
+	later := metav1.NewTime(before.Add(1 * time.Second))
+
+	tests := []struct {
+		obj1     client.Object
+		obj2     client.Object
+		name     string
+		expected bool
+	}{
+		{
+			name: "first is less by timestamp",
+			obj1: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:         "ns1",
+					Name:              "obj1",
+					CreationTimestamp: before,
+				},
+			},
+			obj2: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:         "ns1",
+					Name:              "obj2",
+					CreationTimestamp: later,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "first is less by namespace",
+			obj1: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:         "ns1",
+					Name:              "obj1",
+					CreationTimestamp: before,
+				},
+			},
+			obj2: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:         "ns2",
+					Name:              "obj1",
+					CreationTimestamp: before,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "first is less by name",
+			obj1: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:         "ns1",
+					Name:              "obj1",
+					CreationTimestamp: before,
+				},
+			},
+			obj2: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:         "ns1",
+					Name:              "obj2",
+					CreationTimestamp: before,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "equal",
+			obj1: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:         "ns1",
+					Name:              "obj1",
+					CreationTimestamp: before,
+				},
+			},
+			obj2: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:         "ns1",
+					Name:              "obj1",
+					CreationTimestamp: before,
+				},
+			},
+			expected: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			result := LessClientObject(test.obj1, test.obj2)
+			invertedResult := LessClientObject(test.obj2, test.obj1)
+
+			g.Expect(result).To(Equal(test.expected))
+			g.Expect(invertedResult).To(BeFalse())
+		})
+	}
+}
diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go
index 6c65ff37..55073b00 100644
--- a/internal/mode/static/state/change_processor_test.go
+++ b/internal/mode/static/state/change_processor_test.go
@@ -9,6 +9,7 @@ import (
 	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -23,6 +24,7 @@ import (
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
@@ -187,6 +189,7 @@ func createAlwaysValidValidators() validation.Validators {
 	return validation.Validators{
 		HTTPFieldsValidator: &validationfakes.FakeHTTPFieldsValidator{},
 		GenericValidator:    &validationfakes.FakeGenericValidator{},
+		PolicyValidator:     &validationfakes.FakePolicyValidator{},
 	}
 }
 
@@ -287,7 +290,7 @@ var _ = Describe("ChangeProcessor", func() {
 				GatewayClassName: gcName,
 				Logger:           zap.New(),
 				Validators:       createAlwaysValidValidators(),
-				Scheme:           createScheme(),
+				MustExtractGVK:   kinds.NewMustExtractGKV(createScheme()),
 			})
 		})
 
@@ -338,7 +341,7 @@ var _ = Describe("ChangeProcessor", func() {
 						From: []v1beta1.ReferenceGrantFrom{
 							{
 								Group:     v1.GroupName,
-								Kind:      "Gateway",
+								Kind:      kinds.Gateway,
 								Namespace: "test",
 							},
 						},
@@ -359,7 +362,7 @@ var _ = Describe("ChangeProcessor", func() {
 						From: []v1beta1.ReferenceGrantFrom{
 							{
 								Group:     v1.GroupName,
-								Kind:      "HTTPRoute",
+								Kind:      kinds.HTTPRoute,
 								Namespace: "test",
 							},
 						},
@@ -520,7 +523,7 @@ var _ = Describe("ChangeProcessor", func() {
 								Valid:          true,
 								Attachable:     true,
 								Routes:         map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1},
-								SupportedKinds: []v1.RouteGroupKind{{Kind: "HTTPRoute"}},
+								SupportedKinds: []v1.RouteGroupKind{{Kind: kinds.HTTPRoute}},
 							},
 							{
 								Name:           "listener-443-1",
@@ -529,7 +532,7 @@ var _ = Describe("ChangeProcessor", func() {
 								Attachable:     true,
 								Routes:         map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1},
 								ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(diffNsTLSSecret)),
-								SupportedKinds: []v1.RouteGroupKind{{Kind: "HTTPRoute"}},
+								SupportedKinds: []v1.RouteGroupKind{{Kind: kinds.HTTPRoute}},
 							},
 						},
 						Valid: true,
@@ -1451,7 +1454,7 @@ var _ = Describe("ChangeProcessor", func() {
 					GatewayClassName: gcName,
 					Logger:           zap.New(),
 					Validators:       createAlwaysValidValidators(),
-					Scheme:           createScheme(),
+					MustExtractGVK:   kinds.NewMustExtractGKV(createScheme()),
 				})
 				processor.CaptureUpsertChange(gc)
 				processor.CaptureUpsertChange(gw)
@@ -1500,16 +1503,19 @@ var _ = Describe("ChangeProcessor", func() {
 					Expect(changed).To(Equal(state.ClusterStateChange))
 				})
 			})
-			When("a namespace that is linked to a listener has its labels changed to no longer match a listener", func() {
-				It("triggers an update", func() {
-					nsDifferentLabels.Labels = map[string]string{
-						"oranges": "bananas",
-					}
-					processor.CaptureUpsertChange(nsDifferentLabels)
-					changed, _ := processor.Process()
-					Expect(changed).To(Equal(state.ClusterStateChange))
-				})
-			})
+			When(
+				"a namespace that is linked to a listener has its labels changed to no longer match a listener",
+				func() {
+					It("triggers an update", func() {
+						nsDifferentLabels.Labels = map[string]string{
+							"oranges": "bananas",
+						}
+						processor.CaptureUpsertChange(nsDifferentLabels)
+						changed, _ := processor.Process()
+						Expect(changed).To(Equal(state.ClusterStateChange))
+					})
+				},
+			)
 			When("a gateway changes its listener's labels", func() {
 				It("triggers an update when a namespace that matches the new labels is created", func() {
 					gwChangedLabel := gw.DeepCopy()
@@ -1559,7 +1565,7 @@ var _ = Describe("ChangeProcessor", func() {
 			paramGC := gc.DeepCopy()
 			paramGC.Spec.ParametersRef = &v1beta1.ParametersReference{
 				Group: ngfAPI.GroupName,
-				Kind:  v1beta1.Kind("NginxProxy"),
+				Kind:  v1beta1.Kind(kinds.NginxProxy),
 				Name:  "np",
 			}
 
@@ -1608,8 +1614,98 @@ var _ = Describe("ChangeProcessor", func() {
 				Expect(graph.NginxProxy).To(BeNil())
 			})
 		})
-	})
 
+		Describe("NGF Policy resource changes", Ordered, func() {
+			var (
+				gw              *v1.Gateway
+				csp, cspUpdated *ngfAPI.ClientSettingsPolicy
+				cspKey          graph.PolicyKey
+			)
+
+			BeforeAll(func() {
+				processor.CaptureUpsertChange(gc)
+				changed, newGraph := processor.Process()
+				Expect(changed).To(Equal(state.ClusterStateChange))
+				Expect(newGraph.GatewayClass.Source).To(Equal(gc))
+				Expect(newGraph.NGFPolicies).To(BeEmpty())
+
+				gw = createGateway("gw")
+
+				csp = &ngfAPI.ClientSettingsPolicy{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "csp",
+						Namespace: "test",
+					},
+					Spec: ngfAPI.ClientSettingsPolicySpec{
+						TargetRef: v1alpha2.LocalPolicyTargetReference{
+							Group: v1.GroupName,
+							Kind:  kinds.Gateway,
+							Name:  "gw",
+						},
+						Body: &ngfAPI.ClientBody{
+							MaxSize: helpers.GetPointer[ngfAPI.Size]("10m"),
+						},
+					},
+				}
+
+				cspUpdated = csp.DeepCopy()
+				cspUpdated.Spec.Body.MaxSize = helpers.GetPointer[ngfAPI.Size]("20m")
+
+				cspKey = graph.PolicyKey{
+					NsName: types.NamespacedName{Name: "csp", Namespace: "test"},
+					GVK: schema.GroupVersionKind{
+						Group:   ngfAPI.GroupName,
+						Kind:    kinds.ClientSettingsPolicy,
+						Version: "v1alpha1",
+					},
+				}
+			})
+
+			/*
+				NOTE: When adding a new NGF policy to the change processor,
+				update the following tests to make sure that the change processor can track changes for multiple NGF
+				policies.
+			*/
+
+			When("a policy is created that references a resource that is not in the last graph", func() {
+				It("reports no changes", func() {
+					processor.CaptureUpsertChange(csp)
+
+					changed, _ := processor.Process()
+					Expect(changed).To(Equal(state.NoChange))
+				})
+			})
+			When("the resource the policy references is created", func() {
+				It("populates the graph with the policy", func() {
+					processor.CaptureUpsertChange(gw)
+
+					changed, graph := processor.Process()
+					Expect(changed).To(Equal(state.ClusterStateChange))
+					Expect(graph.NGFPolicies).To(HaveKey(cspKey))
+					Expect(graph.NGFPolicies[cspKey].Source).To(Equal(csp))
+				})
+			})
+			When("the policy is updated", func() {
+				It("captures changes for a policy", func() {
+					processor.CaptureUpsertChange(cspUpdated)
+
+					changed, graph := processor.Process()
+					Expect(changed).To(Equal(state.ClusterStateChange))
+					Expect(graph.NGFPolicies).To(HaveKey(cspKey))
+					Expect(graph.NGFPolicies[cspKey].Source).To(Equal(cspUpdated))
+				})
+			})
+			When("the policy is deleted", func() {
+				It("removes the policy from the graph", func() {
+					processor.CaptureDeleteChange(&ngfAPI.ClientSettingsPolicy{}, client.ObjectKeyFromObject(csp))
+
+					changed, graph := processor.Process()
+					Expect(changed).To(Equal(state.ClusterStateChange))
+					Expect(graph.NGFPolicies).To(BeEmpty())
+				})
+			})
+		})
+	})
 	Describe("Ensuring non-changing changes don't override previously changing changes", func() {
 		// Note: in these tests, we deliberately don't fully inspect the returned configuration and statuses
 		// -- this is done in 'Normal cases of processing changes'
@@ -1636,7 +1732,7 @@ var _ = Describe("ChangeProcessor", func() {
 				GatewayCtlrName:  "test.controller",
 				GatewayClassName: "test-class",
 				Validators:       createAlwaysValidValidators(),
-				Scheme:           createScheme(),
+				MustExtractGVK:   kinds.NewMustExtractGKV(createScheme()),
 			})
 
 			secretNsName = types.NamespacedName{Namespace: "test", Name: "tls-secret"}
@@ -2163,7 +2259,7 @@ var _ = Describe("ChangeProcessor", func() {
 				GatewayCtlrName:  "test.controller",
 				GatewayClassName: "my-class",
 				Validators:       createAlwaysValidValidators(),
-				Scheme:           createScheme(),
+				MustExtractGVK:   kinds.NewMustExtractGKV(createScheme()),
 			})
 		})
 
diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go
index 2fa7b75c..8ea630a2 100644
--- a/internal/mode/static/state/dataplane/configuration_test.go
+++ b/internal/mode/static/state/dataplane/configuration_test.go
@@ -9,6 +9,7 @@ import (
 	. "github.com/onsi/gomega"
 	apiv1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	v1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -17,17 +18,64 @@ import (
 
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver/resolverfakes"
 )
 
+func createFakePolicy(name string, kind string) policies.Policy {
+	fakeKind := &policiesfakes.FakeObjectKind{
+		GroupVersionKindStub: func() schema.GroupVersionKind {
+			return schema.GroupVersionKind{Kind: kind}
+		},
+	}
+
+	return &policiesfakes.FakePolicy{
+		GetNameStub: func() string {
+			return name
+		},
+		GetNamespaceStub: func() string {
+			return "default"
+		},
+		GetObjectKindStub: func() schema.ObjectKind {
+			return fakeKind
+		},
+	}
+}
+
 func TestBuildConfiguration(t *testing.T) {
 	const (
 		invalidMatchesPath = "/not-valid-matches"
 		invalidFiltersPath = "/not-valid-filters"
 	)
 
+	gwPolicy1 := &graph.Policy{
+		Source: createFakePolicy("attach-gw", "ApplePolicy"),
+		Valid:  true,
+	}
+
+	gwPolicy2 := &graph.Policy{
+		Source: createFakePolicy("attach-gw", "OrangePolicy"),
+		Valid:  true,
+	}
+
+	hrPolicy1 := &graph.Policy{
+		Source: createFakePolicy("attach-hr", "LemonPolicy"),
+		Valid:  true,
+	}
+
+	hrPolicy2 := &graph.Policy{
+		Source: createFakePolicy("attach-hr", "LimePolicy"),
+		Valid:  true,
+	}
+
+	invalidPolicy := &graph.Policy{
+		Source: createFakePolicy("invalid", "LimePolicy"),
+		Valid:  false,
+	}
+
 	createRoute := func(name string) *v1.HTTPRoute {
 		return &v1.HTTPRoute{
 			ObjectMeta: metav1.ObjectMeta{
@@ -408,6 +456,30 @@ func TestBuildConfiguration(t *testing.T) {
 		Hostname:     "foo.example.com",
 	}
 
+	hrWithPolicy, expHRWithPolicyGroups, l7RouteWithPolicy := createTestResources(
+		"hr-with-policy",
+		"policy.com",
+		"listener-80-1",
+		pathAndType{
+			path:     "/",
+			pathType: prefix,
+		},
+	)
+
+	l7RouteWithPolicy.Policies = []*graph.Policy{hrPolicy1, invalidPolicy}
+
+	httpsHRWithPolicy, expHTTPSHRWithPolicyGroups, l7HTTPSRouteWithPolicy := createTestResources(
+		"https-hr-with-policy",
+		"policy.com",
+		"listener-443-1",
+		pathAndType{
+			path:     "/",
+			pathType: prefix,
+		},
+	)
+
+	l7HTTPSRouteWithPolicy.Policies = []*graph.Policy{hrPolicy2, invalidPolicy}
+
 	secret1NsName := types.NamespacedName{Namespace: "test", Name: "secret-1"}
 	secret1 := &graph.Secret{
 		Source: &apiv1.Secret{
@@ -1999,13 +2071,204 @@ func TestBuildConfiguration(t *testing.T) {
 			},
 			msg: "NginxProxy with tracing config and http2 disabled",
 		},
+		{
+			graph: &graph.Graph{
+				GatewayClass: &graph.GatewayClass{
+					Source: &v1.GatewayClass{},
+					Valid:  true,
+				},
+				Gateway: &graph.Gateway{
+					Source: &v1.Gateway{},
+					Listeners: []*graph.Listener{
+						{
+							Name:   "listener-80-1",
+							Source: listener80,
+							Valid:  true,
+							Routes: map[graph.RouteKey]*graph.L7Route{
+								graph.CreateRouteKey(hrWithPolicy): l7RouteWithPolicy,
+							},
+						},
+						{
+							Name:   "listener-443",
+							Source: listener443,
+							Valid:  true,
+							Routes: map[graph.RouteKey]*graph.L7Route{
+								graph.CreateRouteKey(httpsHRWithPolicy): l7HTTPSRouteWithPolicy,
+							},
+							ResolvedSecret: &secret1NsName,
+						},
+					},
+					Policies: []*graph.Policy{gwPolicy1, gwPolicy2},
+				},
+				Routes: map[graph.RouteKey]*graph.L7Route{
+					graph.CreateRouteKey(hrWithPolicy):      l7RouteWithPolicy,
+					graph.CreateRouteKey(httpsHRWithPolicy): l7HTTPSRouteWithPolicy,
+				},
+				ReferencedSecrets: map[types.NamespacedName]*graph.Secret{
+					secret1NsName: secret1,
+				},
+			},
+			expConf: Configuration{
+				SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{
+					"ssl_keypair_test_secret-1": {
+						Cert: []byte("cert-1"),
+						Key:  []byte("privateKey-1"),
+					},
+				},
+				CertBundles: map[CertBundleID]CertBundle{},
+				HTTPServers: []VirtualServer{
+					{
+						IsDefault: true,
+						Port:      80,
+						Additions: []Addition{
+							{
+								Bytes:      []byte("apple"),
+								Identifier: "ApplePolicy_default_attach-gw",
+							},
+							{
+								Bytes:      []byte("orange"),
+								Identifier: "OrangePolicy_default_attach-gw",
+							},
+						},
+					},
+					{
+						Hostname: "policy.com",
+						PathRules: []PathRule{
+							{
+								Path:     "/",
+								PathType: PathTypePrefix,
+								MatchRules: []MatchRule{
+									{
+										Source:       &hrWithPolicy.ObjectMeta,
+										BackendGroup: expHRWithPolicyGroups[0],
+										Additions: []Addition{
+											{
+												Bytes:      []byte("lemon"),
+												Identifier: "LemonPolicy_default_attach-hr",
+											},
+										},
+									},
+								},
+							},
+						},
+						Port: 80,
+						Additions: []Addition{
+							{
+								Bytes:      []byte("apple"),
+								Identifier: "ApplePolicy_default_attach-gw",
+							},
+							{
+								Bytes:      []byte("orange"),
+								Identifier: "OrangePolicy_default_attach-gw",
+							},
+						},
+					},
+				},
+				SSLServers: []VirtualServer{
+					{
+						IsDefault: true,
+						Port:      443,
+						Additions: []Addition{
+							{
+								Bytes:      []byte("apple"),
+								Identifier: "ApplePolicy_default_attach-gw",
+							},
+							{
+								Bytes:      []byte("orange"),
+								Identifier: "OrangePolicy_default_attach-gw",
+							},
+						},
+					},
+					{
+						Hostname: "policy.com",
+						PathRules: []PathRule{
+							{
+								Path:     "/",
+								PathType: PathTypePrefix,
+								MatchRules: []MatchRule{
+									{
+										BackendGroup: expHTTPSHRWithPolicyGroups[0],
+										Source:       &httpsHRWithPolicy.ObjectMeta,
+										Additions: []Addition{
+											{
+												Bytes:      []byte("lime"),
+												Identifier: "LimePolicy_default_attach-hr",
+											},
+										},
+									},
+								},
+							},
+						},
+						SSL:  &SSL{KeyPairID: "ssl_keypair_test_secret-1"},
+						Port: 443,
+						Additions: []Addition{
+							{
+								Bytes:      []byte("apple"),
+								Identifier: "ApplePolicy_default_attach-gw",
+							},
+							{
+								Bytes:      []byte("orange"),
+								Identifier: "OrangePolicy_default_attach-gw",
+							},
+						},
+					},
+					{
+						Hostname: wildcardHostname,
+						SSL:      &SSL{KeyPairID: "ssl_keypair_test_secret-1"},
+						Port:     443,
+						Additions: []Addition{
+							{
+								Bytes:      []byte("apple"),
+								Identifier: "ApplePolicy_default_attach-gw",
+							},
+							{
+								Bytes:      []byte("orange"),
+								Identifier: "OrangePolicy_default_attach-gw",
+							},
+						},
+					},
+				},
+				Upstreams: []Upstream{fooUpstream},
+				BackendGroups: []BackendGroup{
+					expHRWithPolicyGroups[0],
+					expHTTPSHRWithPolicyGroups[0],
+				},
+				BaseHTTPConfig: BaseHTTPConfig{
+					HTTP2: true,
+				},
+			},
+			msg: "Simple Gateway and HTTPRoute with policies attached",
+		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.msg, func(t *testing.T) {
 			g := NewWithT(t)
 
-			result := BuildConfiguration(context.TODO(), test.graph, fakeResolver, 1)
+			fakeGenerator := &policiesfakes.FakeConfigGenerator{
+				GenerateStub: func(p policies.Policy) []byte {
+					switch kind := p.GetObjectKind().GroupVersionKind().Kind; kind {
+					case "ApplePolicy":
+						return []byte("apple")
+					case "OrangePolicy":
+						return []byte("orange")
+					case "LemonPolicy":
+						return []byte("lemon")
+					case "LimePolicy":
+						return []byte("lime")
+					default:
+						panic(fmt.Sprintf("unknown policy kind: %s", kind))
+					}
+				},
+			}
+
+			result := BuildConfiguration(
+				context.TODO(),
+				test.graph,
+				fakeResolver,
+				fakeGenerator,
+				1,
+			)
 
 			g.Expect(result.BackendGroups).To(ConsistOf(test.expConf.BackendGroups))
 			g.Expect(result.Upstreams).To(ConsistOf(test.expConf.Upstreams))
@@ -2768,3 +3031,91 @@ func TestBuildTelemetry(t *testing.T) {
 		})
 	}
 }
+
+func TestBuildAdditions(t *testing.T) {
+	getPolicy := func(kind, name string) policies.Policy {
+		return &policiesfakes.FakePolicy{
+			GetNameStub: func() string {
+				return name
+			},
+			GetNamespaceStub: func() string {
+				return "test"
+			},
+			GetObjectKindStub: func() schema.ObjectKind {
+				objKind := &policiesfakes.FakeObjectKind{
+					GroupVersionKindStub: func() schema.GroupVersionKind {
+						return schema.GroupVersionKind{Kind: kind}
+					},
+				}
+
+				return objKind
+			},
+		}
+	}
+
+	tests := []struct {
+		name         string
+		policies     []*graph.Policy
+		expAdditions []Addition
+	}{
+		{
+			name:         "nil policies",
+			policies:     nil,
+			expAdditions: nil,
+		},
+		{
+			name: "mix of valid and invalid policies",
+			policies: []*graph.Policy{
+				{
+					Source: getPolicy("Kind1", "valid1"),
+					Valid:  true,
+				},
+				{
+					Source: getPolicy("Kind2", "valid2"),
+					Valid:  true,
+				},
+				{
+					Source: getPolicy("Kind1", "invalid1"),
+					Valid:  false,
+				},
+				{
+					Source: getPolicy("Kind2", "invalid2"),
+					Valid:  false,
+				},
+				{
+					Source: getPolicy("Kind3", "valid3"),
+					Valid:  true,
+				},
+			},
+			expAdditions: []Addition{
+				{
+					Identifier: "Kind1_test_valid1",
+					Bytes:      []byte("valid1"),
+				},
+				{
+					Identifier: "Kind2_test_valid2",
+					Bytes:      []byte("valid2"),
+				},
+				{
+					Identifier: "Kind3_test_valid3",
+					Bytes:      []byte("valid3"),
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			generator := &policiesfakes.FakeConfigGenerator{
+				GenerateStub: func(policy policies.Policy) []byte {
+					return []byte(policy.GetName())
+				},
+			}
+
+			additions := buildAdditions(test.policies, generator)
+			g.Expect(additions).To(BeEquivalentTo(test.expAdditions))
+		})
+	}
+}
diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go
index 2bee1299..140d7188 100644
--- a/internal/mode/static/state/graph/backend_refs_test.go
+++ b/internal/mode/static/state/graph/backend_refs_test.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 )
 
@@ -107,7 +108,7 @@ func TestValidateBackendRef(t *testing.T) {
 			From: []v1beta1.ReferenceGrantFrom{
 				{
 					Group:     gatewayv1.GroupName,
-					Kind:      "HTTPRoute",
+					Kind:      kinds.HTTPRoute,
 					Namespace: "test",
 				},
 			},
@@ -523,7 +524,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) {
 					Type:    "Accepted",
 					Status:  "True",
 					Reason:  "Accepted",
-					Message: "BackendTLSPolicy is accepted by the Gateway",
+					Message: "Policy is accepted",
 				},
 			},
 			Valid:        true,
@@ -538,7 +539,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) {
 		Type:    "Accepted",
 		Status:  "True",
 		Reason:  "Accepted",
-		Message: "BackendTLSPolicy is accepted by the Gateway",
+		Message: "Policy is accepted",
 	},
 	)
 
@@ -764,7 +765,7 @@ func TestCreateBackend(t *testing.T) {
 		},
 		Valid: false,
 		Conditions: []conditions.Condition{
-			staticConds.NewBackendTLSPolicyInvalid("unsupported value"),
+			staticConds.NewPolicyInvalid("unsupported value"),
 		},
 	}
 
diff --git a/internal/mode/static/state/graph/backend_tls_policy_test.go b/internal/mode/static/state/graph/backend_tls_policy_test.go
index bcf03c98..b4ded8f2 100644
--- a/internal/mode/static/state/graph/backend_tls_policy_test.go
+++ b/internal/mode/static/state/graph/backend_tls_policy_test.go
@@ -12,6 +12,7 @@ import (
 	"sigs.k8s.io/gateway-api/apis/v1alpha3"
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 )
 
 func TestProcessBackendTLSPoliciesEmpty(t *testing.T) {
@@ -129,6 +130,8 @@ func TestValidateBackendTLSPolicy(t *testing.T) {
 			AncestorRef: gatewayv1.ParentReference{
 				Name:      gatewayv1.ObjectName(parentName),
 				Namespace: helpers.GetPointer(gatewayv1.Namespace("test")),
+				Group:     helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
+				Kind:      helpers.GetPointer[gatewayv1.Kind](kinds.Gateway),
 			},
 		}
 	}
@@ -152,7 +155,9 @@ func TestValidateBackendTLSPolicy(t *testing.T) {
 		getAncestorRef("not-us", "not-us"),
 	}
 
-	ancestorsWithUs := append(ancestors, getAncestorRef("test", "gateway"))
+	ancestorsWithUs := make([]v1alpha2.PolicyAncestorStatus, len(ancestors))
+	copy(ancestorsWithUs, ancestors)
+	ancestorsWithUs[0] = getAncestorRef("test", "gateway")
 
 	tests := []struct {
 		tlsPolicy *v1alpha3.BackendTLSPolicy
@@ -392,16 +397,7 @@ func TestValidateBackendTLSPolicy(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			g := NewWithT(t)
 
-			gateway := &Gateway{
-				Source: &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "test"}},
-			}
-
-			valid, ignored, conds := validateBackendTLSPolicy(
-				test.tlsPolicy,
-				configMapResolver,
-				"test",
-				gateway,
-			)
+			valid, ignored, conds := validateBackendTLSPolicy(test.tlsPolicy, configMapResolver, "test")
 
 			g.Expect(valid).To(Equal(test.isValid))
 			g.Expect(ignored).To(Equal(test.ignored))
diff --git a/internal/mode/static/state/graph/gateway_listener_test.go b/internal/mode/static/state/graph/gateway_listener_test.go
index dafe63e5..96aba1a6 100644
--- a/internal/mode/static/state/graph/gateway_listener_test.go
+++ b/internal/mode/static/state/graph/gateway_listener_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 )
 
@@ -290,7 +291,7 @@ func TestValidateListenerHostname(t *testing.T) {
 func TestGetAndValidateListenerSupportedKinds(t *testing.T) {
 	HTTPRouteGroupKind := []v1.RouteGroupKind{
 		{
-			Kind:  "HTTPRoute",
+			Kind:  kinds.HTTPRoute,
 			Group: helpers.GetPointer[v1.Group](v1.GroupName),
 		},
 	}
@@ -318,7 +319,7 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) {
 			protocol: v1.HTTPProtocolType,
 			kind: []v1.RouteGroupKind{
 				{
-					Kind:  "HTTPRoute",
+					Kind:  kinds.HTTPRoute,
 					Group: helpers.GetPointer[v1.Group]("bad-group"),
 				},
 			},
@@ -353,7 +354,7 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) {
 			name:      "valid HTTPS no kind specified",
 			expected: []v1.RouteGroupKind{
 				{
-					Kind: "HTTPRoute",
+					Kind: kinds.HTTPRoute,
 				},
 			},
 		},
@@ -361,7 +362,7 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) {
 			protocol: v1.HTTPProtocolType,
 			kind: []v1.RouteGroupKind{
 				{
-					Kind:  "HTTPRoute",
+					Kind:  kinds.HTTPRoute,
 					Group: helpers.GetPointer[v1.Group](v1.GroupName),
 				},
 				{
diff --git a/internal/mode/static/state/graph/gateway_test.go b/internal/mode/static/state/graph/gateway_test.go
index df10c2aa..8bb89471 100644
--- a/internal/mode/static/state/graph/gateway_test.go
+++ b/internal/mode/static/state/graph/gateway_test.go
@@ -14,6 +14,7 @@ import (
 	v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 )
 
@@ -156,7 +157,7 @@ func TestBuildGateway(t *testing.T) {
 		Protocol: v1.HTTPProtocolType,
 		AllowedRoutes: &v1.AllowedRoutes{
 			Kinds: []v1.RouteGroupKind{
-				{Kind: "HTTPRoute", Group: helpers.GetPointer[v1.Group](v1.GroupName)},
+				{Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group](v1.GroupName)},
 			},
 			Namespaces: &v1.RouteNamespaces{
 				From:     helpers.GetPointer(v1.NamespacesFromSelector),
@@ -297,7 +298,12 @@ func TestBuildGateway(t *testing.T) {
 		443,
 		tlsConfigInvalidSecret,
 	)
-	invalidHTTPSPortListener := createHTTPSListener("invalid-https-port", "foo.example.com", 65536, gatewayTLSConfigSameNs)
+	invalidHTTPSPortListener := createHTTPSListener(
+		"invalid-https-port",
+		"foo.example.com",
+		65536,
+		gatewayTLSConfigSameNs,
+	)
 
 	const (
 		invalidHostnameMsg = `hostname: Invalid value: "$example.com": a lowercase RFC 1123 subdomain ` +
@@ -362,7 +368,7 @@ func TestBuildGateway(t *testing.T) {
 						Attachable: true,
 						Routes:     map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -372,7 +378,7 @@ func TestBuildGateway(t *testing.T) {
 						Attachable: true,
 						Routes:     map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -396,7 +402,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:         map[RouteKey]*L7Route{},
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -407,7 +413,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:         map[RouteKey]*L7Route{},
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -429,7 +435,7 @@ func TestBuildGateway(t *testing.T) {
 						AllowedRouteLabelSelector: labels.SelectorFromSet(labels.Set(labelSet)),
 						Routes:                    map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute", Group: helpers.GetPointer[v1.Group](v1.GroupName)},
+							{Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group](v1.GroupName)},
 						},
 					},
 				},
@@ -450,7 +456,7 @@ func TestBuildGateway(t *testing.T) {
 						From: []v1beta1.ReferenceGrantFrom{
 							{
 								Group:     v1.GroupName,
-								Kind:      "Gateway",
+								Kind:      kinds.Gateway,
 								Namespace: "test",
 							},
 						},
@@ -475,7 +481,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:         map[RouteKey]*L7Route{},
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretDiffNamespace)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -499,7 +505,7 @@ func TestBuildGateway(t *testing.T) {
 						),
 						Routes: map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -523,7 +529,7 @@ func TestBuildGateway(t *testing.T) {
 						),
 						Routes: map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute", Group: helpers.GetPointer[v1.Group](v1.GroupName)},
+							{Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group](v1.GroupName)},
 						},
 					},
 				},
@@ -547,7 +553,7 @@ func TestBuildGateway(t *testing.T) {
 						),
 						Routes: map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -579,7 +585,7 @@ func TestBuildGateway(t *testing.T) {
 						),
 						Routes: map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -592,7 +598,7 @@ func TestBuildGateway(t *testing.T) {
 						),
 						Routes: map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -605,7 +611,7 @@ func TestBuildGateway(t *testing.T) {
 						),
 						Routes: map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -628,7 +634,7 @@ func TestBuildGateway(t *testing.T) {
 						Conditions: staticConds.NewListenerUnsupportedValue(invalidHostnameMsg),
 						Routes:     map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -638,7 +644,7 @@ func TestBuildGateway(t *testing.T) {
 						Conditions: staticConds.NewListenerUnsupportedValue(invalidHostnameMsg),
 						Routes:     map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -662,7 +668,7 @@ func TestBuildGateway(t *testing.T) {
 							`tls.certificateRefs[0]: Invalid value: test/does-not-exist: secret does not exist`,
 						),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -696,7 +702,7 @@ func TestBuildGateway(t *testing.T) {
 						Attachable: true,
 						Routes:     map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -706,7 +712,7 @@ func TestBuildGateway(t *testing.T) {
 						Attachable: true,
 						Routes:     map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -716,7 +722,7 @@ func TestBuildGateway(t *testing.T) {
 						Attachable: true,
 						Routes:     map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -727,7 +733,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:         map[RouteKey]*L7Route{},
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -738,7 +744,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:         map[RouteKey]*L7Route{},
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -748,7 +754,7 @@ func TestBuildGateway(t *testing.T) {
 						Attachable: true,
 						Routes:     map[RouteKey]*L7Route{},
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -759,7 +765,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:         map[RouteKey]*L7Route{},
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -770,7 +776,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:         map[RouteKey]*L7Route{},
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
@@ -803,7 +809,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:     map[RouteKey]*L7Route{},
 						Conditions: staticConds.NewListenerProtocolConflict(conflict80PortMsg),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -814,7 +820,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:     map[RouteKey]*L7Route{},
 						Conditions: staticConds.NewListenerProtocolConflict(conflict80PortMsg),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -825,7 +831,7 @@ func TestBuildGateway(t *testing.T) {
 						Routes:     map[RouteKey]*L7Route{},
 						Conditions: staticConds.NewListenerProtocolConflict(conflict443PortMsg),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -837,7 +843,7 @@ func TestBuildGateway(t *testing.T) {
 						Conditions:     staticConds.NewListenerProtocolConflict(conflict80PortMsg),
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -849,7 +855,7 @@ func TestBuildGateway(t *testing.T) {
 						Conditions:     staticConds.NewListenerProtocolConflict(conflict443PortMsg),
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 					{
@@ -861,7 +867,7 @@ func TestBuildGateway(t *testing.T) {
 						Conditions:     staticConds.NewListenerProtocolConflict(conflict443PortMsg),
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)),
 						SupportedKinds: []v1.RouteGroupKind{
-							{Kind: "HTTPRoute"},
+							{Kind: kinds.HTTPRoute},
 						},
 					},
 				},
diff --git a/internal/mode/static/state/graph/gatewayclass_test.go b/internal/mode/static/state/graph/gatewayclass_test.go
index a60da7ee..5ec28371 100644
--- a/internal/mode/static/state/graph/gatewayclass_test.go
+++ b/internal/mode/static/state/graph/gatewayclass_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes"
@@ -129,7 +130,7 @@ func TestBuildGatewayClass(t *testing.T) {
 	gcWithParams := &v1.GatewayClass{
 		Spec: v1.GatewayClassSpec{
 			ParametersRef: &v1.ParametersReference{
-				Kind:      v1.Kind("NginxProxy"),
+				Kind:      v1.Kind(kinds.NginxProxy),
 				Namespace: helpers.GetPointer(v1.Namespace("test")),
 				Name:      "nginx-proxy",
 			},
@@ -207,7 +208,7 @@ func TestBuildGatewayClass(t *testing.T) {
 			gc: gcWithParams,
 			np: &ngfAPI.NginxProxy{
 				TypeMeta: metav1.TypeMeta{
-					Kind: "NginxProxy",
+					Kind: kinds.NginxProxy,
 				},
 				Spec: ngfAPI.NginxProxySpec{
 					Telemetry: &ngfAPI.Telemetry{
@@ -227,7 +228,7 @@ func TestBuildGatewayClass(t *testing.T) {
 			gc: gcWithInvalidKind,
 			np: &ngfAPI.NginxProxy{
 				TypeMeta: metav1.TypeMeta{
-					Kind: "NginxProxy",
+					Kind: kinds.NginxProxy,
 				},
 			},
 			expected: &GatewayClass{
@@ -259,7 +260,7 @@ func TestBuildGatewayClass(t *testing.T) {
 			gc: gcWithParams,
 			np: &ngfAPI.NginxProxy{
 				TypeMeta: metav1.TypeMeta{
-					Kind: "NginxProxy",
+					Kind: kinds.NginxProxy,
 				},
 				Spec: ngfAPI.NginxProxySpec{
 					Telemetry: &ngfAPI.Telemetry{
diff --git a/internal/mode/static/state/graph/graph_test.go b/internal/mode/static/state/graph/graph_test.go
index 99332fc4..e8097270 100644
--- a/internal/mode/static/state/graph/graph_test.go
+++ b/internal/mode/static/state/graph/graph_test.go
@@ -9,6 +9,7 @@ import (
 	discoveryV1 "k8s.io/api/discovery/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -20,6 +21,9 @@ import (
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes"
@@ -31,6 +35,8 @@ func TestBuildGraph(t *testing.T) {
 		controllerName = "my.controller"
 	)
 
+	testNs := "test"
+
 	protectedPorts := ProtectedPorts{
 		9113: "MetricsPort",
 		8081: "HealthPort",
@@ -47,9 +53,9 @@ func TestBuildGraph(t *testing.T) {
 	}
 
 	btpAcceptedConds := []conditions.Condition{
-		staticConds.NewBackendTLSPolicyAccepted(),
-		staticConds.NewBackendTLSPolicyAccepted(),
-		staticConds.NewBackendTLSPolicyAccepted(),
+		staticConds.NewPolicyAccepted(),
+		staticConds.NewPolicyAccepted(),
+		staticConds.NewPolicyAccepted(),
 	}
 
 	btp := BackendTLSPolicy{
@@ -82,7 +88,7 @@ func TestBuildGraph(t *testing.T) {
 		},
 		Valid:        true,
 		IsReferenced: true,
-		Gateway:      types.NamespacedName{Namespace: "test", Name: "gateway-1"},
+		Gateway:      types.NamespacedName{Namespace: testNs, Name: "gateway-1"},
 		Conditions:   btpAcceptedConds,
 		CaCertRef:    types.NamespacedName{Namespace: "service", Name: "configmap"},
 	}
@@ -132,14 +138,14 @@ func TestBuildGraph(t *testing.T) {
 	createRoute := func(name string, gatewayName string, listenerName string) *gatewayv1.HTTPRoute {
 		return &gatewayv1.HTTPRoute{
 			ObjectMeta: metav1.ObjectMeta{
-				Namespace: "test",
+				Namespace: testNs,
 				Name:      name,
 			},
 			Spec: gatewayv1.HTTPRouteSpec{
 				CommonRouteSpec: gatewayv1.CommonRouteSpec{
 					ParentRefs: []gatewayv1.ParentReference{
 						{
-							Namespace:   (*gatewayv1.Namespace)(helpers.GetPointer("test")),
+							Namespace:   (*gatewayv1.Namespace)(helpers.GetPointer(testNs)),
 							Name:        gatewayv1.ObjectName(gatewayName),
 							SectionName: (*gatewayv1.SectionName)(helpers.GetPointer(listenerName)),
 						},
@@ -168,14 +174,14 @@ func TestBuildGraph(t *testing.T) {
 
 	gr := &gatewayv1.GRPCRoute{
 		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "test",
+			Namespace: testNs,
 			Name:      "gr",
 		},
 		Spec: gatewayv1.GRPCRouteSpec{
 			CommonRouteSpec: gatewayv1.CommonRouteSpec{
 				ParentRefs: []gatewayv1.ParentReference{
 					{
-						Namespace:   (*gatewayv1.Namespace)(helpers.GetPointer("test")),
+						Namespace:   (*gatewayv1.Namespace)(helpers.GetPointer(testNs)),
 						Name:        gatewayv1.ObjectName("gateway-1"),
 						SectionName: (*gatewayv1.SectionName)(helpers.GetPointer("listener-80-1")),
 					},
@@ -198,7 +204,7 @@ func TestBuildGraph(t *testing.T) {
 
 	secret := &v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "test",
+			Namespace: testNs,
 			Name:      "secret",
 		},
 		Data: map[string][]byte{
@@ -210,7 +216,7 @@ func TestBuildGraph(t *testing.T) {
 
 	ns := &v1.Namespace{
 		ObjectMeta: metav1.ObjectMeta{
-			Name: "test",
+			Name: testNs,
 			Labels: map[string]string{
 				"app": "allowed",
 			},
@@ -220,7 +226,7 @@ func TestBuildGraph(t *testing.T) {
 	createGateway := func(name string) *gatewayv1.Gateway {
 		return &gatewayv1.Gateway{
 			ObjectMeta: metav1.ObjectMeta{
-				Namespace: "test",
+				Namespace: testNs,
 				Name:      name,
 			},
 			Spec: gatewayv1.GatewaySpec{
@@ -289,8 +295,8 @@ func TestBuildGraph(t *testing.T) {
 			From: []v1beta1.ReferenceGrantFrom{
 				{
 					Group:     gatewayv1.GroupName,
-					Kind:      "Gateway",
-					Namespace: "test",
+					Kind:      kinds.Gateway,
+					Namespace: gatewayv1.Namespace(testNs),
 				},
 			},
 			To: []v1beta1.ReferenceGrantTo{
@@ -310,8 +316,8 @@ func TestBuildGraph(t *testing.T) {
 			From: []v1beta1.ReferenceGrantFrom{
 				{
 					Group:     gatewayv1.GroupName,
-					Kind:      "HTTPRoute",
-					Namespace: "test",
+					Kind:      kinds.HTTPRoute,
+					Namespace: gatewayv1.Namespace(testNs),
 				},
 			},
 			To: []v1beta1.ReferenceGrantTo{
@@ -339,6 +345,70 @@ func TestBuildGraph(t *testing.T) {
 		},
 	}
 
+	// NGF Policies
+	//
+	// We have to use real policies here instead of a mocks because the Diff function we use in the test fails when
+	// using a mock because the mock has unexported fields.
+	// Testing one type of policy per attachment point should suffice.
+	polGVK := schema.GroupVersionKind{Kind: kinds.ClientSettingsPolicy}
+	hrPolicyKey := PolicyKey{GVK: polGVK, NsName: types.NamespacedName{Namespace: testNs, Name: "hrPolicy"}}
+	hrPolicy := &ngfAPI.ClientSettingsPolicy{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "hrPolicy",
+			Namespace: testNs,
+		},
+		TypeMeta: metav1.TypeMeta{Kind: kinds.ClientSettingsPolicy},
+		Spec: ngfAPI.ClientSettingsPolicySpec{
+			TargetRef: createTestRef(kinds.HTTPRoute, gatewayv1.GroupName, "hr-1"),
+		},
+	}
+	processedRoutePolicy := &Policy{
+		Source: hrPolicy,
+		Ancestor: &PolicyAncestor{
+			Ancestor: gatewayv1.ParentReference{
+				Group:     helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
+				Kind:      helpers.GetPointer[gatewayv1.Kind](kinds.HTTPRoute),
+				Namespace: (*gatewayv1.Namespace)(&testNs),
+				Name:      "hr-1",
+			},
+		},
+		TargetRef: PolicyTargetRef{
+			Kind:   kinds.HTTPRoute,
+			Group:  gatewayv1.GroupName,
+			Nsname: types.NamespacedName{Namespace: testNs, Name: "hr-1"},
+		},
+		Valid: true,
+	}
+
+	gwPolicyKey := PolicyKey{GVK: polGVK, NsName: types.NamespacedName{Namespace: testNs, Name: "gwPolicy"}}
+	gwPolicy := &ngfAPI.ClientSettingsPolicy{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "gwPolicy",
+			Namespace: testNs,
+		},
+		TypeMeta: metav1.TypeMeta{Kind: kinds.ClientSettingsPolicy},
+		Spec: ngfAPI.ClientSettingsPolicySpec{
+			TargetRef: createTestRef(kinds.Gateway, gatewayv1.GroupName, "gateway-1"),
+		},
+	}
+	processedGwPolicy := &Policy{
+		Source: gwPolicy,
+		Ancestor: &PolicyAncestor{
+			Ancestor: gatewayv1.ParentReference{
+				Group:     helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
+				Kind:      helpers.GetPointer[gatewayv1.Kind](kinds.Gateway),
+				Namespace: (*gatewayv1.Namespace)(&testNs),
+				Name:      "gateway-1",
+			},
+		},
+		TargetRef: PolicyTargetRef{
+			Kind:   kinds.Gateway,
+			Group:  gatewayv1.GroupName,
+			Nsname: types.NamespacedName{Namespace: testNs, Name: "gateway-1"},
+		},
+		Valid: true,
+	}
+
 	createStateWithGatewayClass := func(gc *gatewayv1.GatewayClass) ClusterState {
 		return ClusterState{
 			GatewayClasses: map[types.NamespacedName]*gatewayv1.GatewayClass{
@@ -378,6 +448,10 @@ func TestBuildGraph(t *testing.T) {
 			NginxProxies: map[types.NamespacedName]*ngfAPI.NginxProxy{
 				client.ObjectKeyFromObject(proxy): proxy,
 			},
+			NGFPolicies: map[PolicyKey]policies.Policy{
+				hrPolicyKey: hrPolicy,
+				gwPolicyKey: gwPolicy,
+			},
 		}
 	}
 
@@ -401,6 +475,7 @@ func TestBuildGraph(t *testing.T) {
 			Hostnames: hr1.Spec.Hostnames,
 			Rules:     []RouteRule{createValidRuleWithBackendRefs(routeMatches)},
 		},
+		Policies: []*Policy{processedRoutePolicy},
 	}
 
 	routeGR := &L7Route{
@@ -468,7 +543,7 @@ func TestBuildGraph(t *testing.T) {
 							CreateRouteKey(hr1): routeHR1,
 							CreateRouteKey(gr):  routeGR,
 						},
-						SupportedKinds:            []gatewayv1.RouteGroupKind{{Kind: "HTTPRoute"}},
+						SupportedKinds:            []gatewayv1.RouteGroupKind{{Kind: kinds.HTTPRoute}},
 						AllowedRouteLabelSelector: labels.SelectorFromSet(map[string]string{"app": "allowed"}),
 					},
 					{
@@ -478,13 +553,14 @@ func TestBuildGraph(t *testing.T) {
 						Attachable:     true,
 						Routes:         map[RouteKey]*L7Route{CreateRouteKey(hr3): routeHR3},
 						ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secret)),
-						SupportedKinds: []gatewayv1.RouteGroupKind{{Kind: "HTTPRoute"}},
+						SupportedKinds: []gatewayv1.RouteGroupKind{{Kind: kinds.HTTPRoute}},
 					},
 				},
-				Valid: true,
+				Valid:    true,
+				Policies: []*Policy{processedGwPolicy},
 			},
 			IgnoredGateways: map[types.NamespacedName]*gatewayv1.Gateway{
-				{Namespace: "test", Name: "gateway-2"}: gw2,
+				{Namespace: testNs, Name: "gateway-2"}: gw2,
 			},
 			Routes: map[RouteKey]*L7Route{
 				CreateRouteKey(hr1): routeHR1,
@@ -512,6 +588,10 @@ func TestBuildGraph(t *testing.T) {
 				client.ObjectKeyFromObject(btp.Source): &btp,
 			},
 			NginxProxy: proxy,
+			NGFPolicies: map[PolicyKey]*Policy{
+				hrPolicyKey: processedRoutePolicy,
+				gwPolicyKey: processedGwPolicy,
+			},
 		}
 	}
 
@@ -523,7 +603,7 @@ func TestBuildGraph(t *testing.T) {
 			ControllerName: controllerName,
 			ParametersRef: &gatewayv1.ParametersReference{
 				Group: gatewayv1.Group("gateway.nginx.org"),
-				Kind:  gatewayv1.Kind("NginxProxy"),
+				Kind:  gatewayv1.Kind(kinds.NginxProxy),
 				Name:  "nginx-proxy",
 			},
 		},
@@ -565,6 +645,7 @@ func TestBuildGraph(t *testing.T) {
 				validation.Validators{
 					HTTPFieldsValidator: &validationfakes.FakeHTTPFieldsValidator{},
 					GenericValidator:    &validationfakes.FakeGenericValidator{},
+					PolicyValidator:     &validationfakes.FakePolicyValidator{},
 				},
 				protectedPorts,
 			)
@@ -577,13 +658,13 @@ func TestBuildGraph(t *testing.T) {
 func TestIsReferenced(t *testing.T) {
 	baseSecret := &v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "test",
+			Namespace: testNs,
 			Name:      "secret",
 		},
 	}
 	sameNamespaceDifferentNameSecret := &v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "test",
+			Namespace: testNs,
 			Name:      "secret-different-name",
 		},
 	}
@@ -596,7 +677,7 @@ func TestIsReferenced(t *testing.T) {
 
 	nsInGraph := &v1.Namespace{
 		ObjectMeta: metav1.ObjectMeta{
-			Name: "test",
+			Name: testNs,
 			Labels: map[string]string{
 				"app": "allowed",
 			},
@@ -666,13 +747,13 @@ func TestIsReferenced(t *testing.T) {
 
 	baseConfigMap := &v1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "test",
+			Namespace: testNs,
 			Name:      "configmap",
 		},
 	}
 	sameNamespaceDifferentNameConfigMap := &v1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "test",
+			Namespace: testNs,
 			Name:      "configmap-different-name",
 		},
 	}
@@ -688,7 +769,7 @@ func TestIsReferenced(t *testing.T) {
 			Spec: gatewayv1.GatewayClassSpec{
 				ParametersRef: &gatewayv1.ParametersReference{
 					Group: ngfAPI.GroupName,
-					Kind:  gatewayv1.Kind("NginxProxy"),
+					Kind:  gatewayv1.Kind(kinds.NginxProxy),
 					Name:  "nginx-proxy-in-gc",
 				},
 			},
@@ -876,3 +957,167 @@ func TestIsReferenced(t *testing.T) {
 		})
 	}
 }
+
+func TestIsNGFPolicyRelevant(t *testing.T) {
+	policyGVK := schema.GroupVersionKind{Kind: "MyKind"}
+	existingPolicyNsName := types.NamespacedName{Namespace: "test", Name: "pol"}
+
+	hrKey := RouteKey{RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: "test", Name: "hr"}}
+	grKey := RouteKey{RouteType: RouteTypeGRPC, NamespacedName: types.NamespacedName{Namespace: "test", Name: "gr"}}
+
+	getGraph := func() *Graph {
+		return &Graph{
+			Gateway: &Gateway{
+				Source: &gatewayv1.Gateway{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "gw",
+						Namespace: "test",
+					},
+				},
+			},
+			IgnoredGateways: map[types.NamespacedName]*gatewayv1.Gateway{
+				{Namespace: "test", Name: "ignored"}: {},
+			},
+			Routes: map[RouteKey]*L7Route{
+				hrKey: {},
+				grKey: {},
+			},
+			NGFPolicies: map[PolicyKey]*Policy{
+				{GVK: policyGVK, NsName: existingPolicyNsName}: {
+					Source: &policiesfakes.FakePolicy{},
+				},
+			},
+		}
+	}
+
+	type modFunc func(g *Graph) *Graph
+
+	getModifiedGraph := func(mod modFunc) *Graph {
+		return mod(getGraph())
+	}
+
+	getPolicy := func(ref v1alpha2.LocalPolicyTargetReference) policies.Policy {
+		return &policiesfakes.FakePolicy{
+			GetNamespaceStub: func() string {
+				return testNs
+			},
+			GetTargetRefStub: func() v1alpha2.LocalPolicyTargetReference {
+				return ref
+			},
+		}
+	}
+
+	tests := []struct {
+		name        string
+		graph       *Graph
+		policy      policies.Policy
+		nsname      types.NamespacedName
+		expRelevant bool
+	}{
+		{
+			name:        "relevant; policy exists in graph",
+			graph:       getGraph(),
+			policy:      &policiesfakes.FakePolicy{},
+			nsname:      existingPolicyNsName,
+			expRelevant: true,
+		},
+		{
+			name:        "irrelevant; policy does not exist in graph and is empty (delete event)",
+			graph:       getGraph(),
+			policy:      &policiesfakes.FakePolicy{},
+			nsname:      types.NamespacedName{Namespace: "diff", Name: "diff"},
+			expRelevant: false,
+		},
+		{
+			name:        "relevant; policy references the winning gateway",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "gw")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "ref-gw"},
+			expRelevant: true,
+		},
+		{
+			name:        "relevant; policy references an ignored gateway",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "ignored")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "ref-ignored"},
+			expRelevant: true,
+		},
+		{
+			name:        "relevant; policy references an httproute in the graph",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef(kinds.HTTPRoute, gatewayv1.GroupName, "hr")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "ref-hr"},
+			expRelevant: true,
+		},
+		{
+			name:        "relevant; policy references a grpcroute in the graph",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef(kinds.GRPCRoute, gatewayv1.GroupName, "gr")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "ref-gr"},
+			expRelevant: true,
+		},
+		{
+			name:        "irrelevant; policy does not reference a relevant gw or route in the graph",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "diff")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "not-relevant"},
+			expRelevant: false,
+		},
+		{
+			name:        "irrelevant; policy references an unsupported kind in the Gateway group",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef("GatewayClass", gatewayv1.GroupName, "diff")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "unsupported-kind"},
+			expRelevant: false,
+		},
+		{
+			name:        "irrelevant; policy references an unsupported group",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef(kinds.Gateway, "SomeGroup", "diff")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "unsupported-group"},
+			expRelevant: false,
+		},
+		{
+			name: "irrelevant; policy references a Gateway, but the graph's Gateway is nil",
+			graph: getModifiedGraph(func(g *Graph) *Graph {
+				g.Gateway = nil
+				return g
+			}),
+			policy:      getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "diff")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "nil-gw"},
+			expRelevant: false,
+		},
+		{
+			name: "irrelevant; policy references a Gateway, but the graph's Gateway.Source is nil",
+			graph: getModifiedGraph(func(g *Graph) *Graph {
+				g.Gateway.Source = nil
+				return g
+			}),
+			policy:      getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "diff")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "nil-gw-source"},
+			expRelevant: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			relevant := test.graph.IsNGFPolicyRelevant(test.policy, policyGVK, test.nsname)
+			g.Expect(relevant).To(Equal(test.expRelevant))
+		})
+	}
+}
+
+func TestIsNGFPolicyRelevantPanics(t *testing.T) {
+	g := NewWithT(t)
+	graph := &Graph{}
+	nsname := types.NamespacedName{Namespace: "test", Name: "pol"}
+	gvk := schema.GroupVersionKind{Kind: "MyKind"}
+
+	isRelevant := func() {
+		_ = graph.IsNGFPolicyRelevant(nil, gvk, nsname)
+	}
+
+	g.Expect(isRelevant).To(Panic())
+}
diff --git a/internal/mode/static/state/graph/nginxproxy_test.go b/internal/mode/static/state/graph/nginxproxy_test.go
index 80e66da5..6c0f3d52 100644
--- a/internal/mode/static/state/graph/nginxproxy_test.go
+++ b/internal/mode/static/state/graph/nginxproxy_test.go
@@ -11,6 +11,7 @@ import (
 
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes"
 )
 
@@ -35,7 +36,7 @@ func TestGetNginxProxy(t *testing.T) {
 				Spec: v1.GatewayClassSpec{
 					ParametersRef: &v1.ParametersReference{
 						Group: ngfAPI.GroupName,
-						Kind:  v1.Kind("NginxProxy"),
+						Kind:  v1.Kind(kinds.NginxProxy),
 						Name:  "np1",
 					},
 				},
@@ -60,7 +61,7 @@ func TestGetNginxProxy(t *testing.T) {
 				Spec: v1.GatewayClassSpec{
 					ParametersRef: &v1.ParametersReference{
 						Group: ngfAPI.GroupName,
-						Kind:  v1.Kind("NginxProxy"),
+						Kind:  v1.Kind(kinds.NginxProxy),
 						Name:  "np2",
 					},
 				},
@@ -96,7 +97,7 @@ func TestIsNginxProxyReferenced(t *testing.T) {
 					Spec: v1.GatewayClassSpec{
 						ParametersRef: &v1.ParametersReference{
 							Group: ngfAPI.GroupName,
-							Kind:  v1.Kind("NginxProxy"),
+							Kind:  v1.Kind(kinds.NginxProxy),
 							Name:  "nginx-proxy",
 						},
 					},
@@ -126,7 +127,7 @@ func TestIsNginxProxyReferenced(t *testing.T) {
 					Spec: v1.GatewayClassSpec{
 						ParametersRef: &v1.ParametersReference{
 							Group: ngfAPI.GroupName,
-							Kind:  v1.Kind("NginxProxy"),
+							Kind:  v1.Kind(kinds.NginxProxy),
 							Name:  "nginx-proxy",
 						},
 					},
@@ -170,7 +171,7 @@ func TestGCReferencesAnyNginxProxy(t *testing.T) {
 				Spec: v1.GatewayClassSpec{
 					ParametersRef: &v1.ParametersReference{
 						Group: v1.Group("wrong-group"),
-						Kind:  v1.Kind("NginxProxy"),
+						Kind:  v1.Kind(kinds.NginxProxy),
 						Name:  "wrong-group",
 					},
 				},
@@ -196,7 +197,7 @@ func TestGCReferencesAnyNginxProxy(t *testing.T) {
 				Spec: v1.GatewayClassSpec{
 					ParametersRef: &v1.ParametersReference{
 						Group: ngfAPI.GroupName,
-						Kind:  v1.Kind("NginxProxy"),
+						Kind:  v1.Kind(kinds.NginxProxy),
 						Name:  "nginx-proxy",
 					},
 				},
@@ -297,7 +298,9 @@ func TestValidateNginxProxy(t *testing.T) {
 				Spec: ngfAPI.NginxProxySpec{
 					Telemetry: &ngfAPI.Telemetry{
 						Exporter: &ngfAPI.TelemetryExporter{
-							Interval: helpers.GetPointer[ngfAPI.Duration]("my-interval"), // any value is invalid by the validator
+							Interval: helpers.GetPointer[ngfAPI.Duration](
+								"my-interval",
+							), // any value is invalid by the validator
 						},
 					},
 				},
diff --git a/internal/mode/static/state/graph/policies_test.go b/internal/mode/static/state/graph/policies_test.go
new file mode 100644
index 00000000..a2ee6020
--- /dev/null
+++ b/internal/mode/static/state/graph/policies_test.go
@@ -0,0 +1,870 @@
+package graph
+
+import (
+	"errors"
+	"slices"
+	"testing"
+
+	. "github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes"
+	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
+)
+
+var testNs = "test"
+
+func TestAttachPolicies(t *testing.T) {
+	policyGVK := schema.GroupVersionKind{Group: "Group", Version: "Version", Kind: "Policy"}
+
+	gwPolicyKey := createTestPolicyKey(policyGVK, "gw-policy")
+	gwPolicy := &Policy{
+		Valid:  true,
+		Source: &policiesfakes.FakePolicy{},
+		TargetRef: PolicyTargetRef{
+			Kind:   kinds.Gateway,
+			Group:  v1.GroupName,
+			Nsname: types.NamespacedName{Namespace: testNs, Name: "gateway"},
+		},
+	}
+
+	routePolicyKey := createTestPolicyKey(policyGVK, "route-policy")
+	routePolicy := &Policy{
+		Valid:  true,
+		Source: &policiesfakes.FakePolicy{},
+		TargetRef: PolicyTargetRef{
+			Kind:   kinds.HTTPRoute,
+			Group:  v1.GroupName,
+			Nsname: types.NamespacedName{Namespace: testNs, Name: "hr-route"},
+		},
+	}
+
+	grpcRoutePolicyKey := createTestPolicyKey(policyGVK, "grpc-route-policy")
+	grpcRoutePolicy := &Policy{
+		Valid:  true,
+		Source: &policiesfakes.FakePolicy{},
+		TargetRef: PolicyTargetRef{
+			Kind:   kinds.GRPCRoute,
+			Group:  v1.GroupName,
+			Nsname: types.NamespacedName{Namespace: testNs, Name: "grpc-route"},
+		},
+	}
+
+	ngfPolicies := map[PolicyKey]*Policy{
+		gwPolicyKey:        gwPolicy,
+		routePolicyKey:     routePolicy,
+		grpcRoutePolicyKey: grpcRoutePolicy,
+	}
+
+	createRouteKey := func(name string, routeType RouteType) RouteKey {
+		return RouteKey{
+			NamespacedName: types.NamespacedName{Name: name, Namespace: testNs},
+			RouteType:      routeType,
+		}
+	}
+
+	newGraph := func() *Graph {
+		return &Graph{
+			Gateway: &Gateway{
+				Source: &v1.Gateway{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "gateway",
+						Namespace: testNs,
+					},
+				},
+				Valid: true,
+			},
+			Routes: map[RouteKey]*L7Route{
+				createRouteKey("hr-route", RouteTypeHTTP): {
+					Source: &v1.HTTPRoute{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "hr-route",
+							Namespace: testNs,
+						},
+					},
+					ParentRefs: []ParentRef{
+						{
+							Attachment: &ParentRefAttachmentStatus{
+								Attached: true,
+							},
+						},
+					},
+					Valid:      true,
+					Attachable: true,
+				},
+				createRouteKey("grpc-route", RouteTypeGRPC): {
+					Source: &v1alpha2.GRPCRoute{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "grpc-route",
+							Namespace: testNs,
+						},
+					},
+					ParentRefs: []ParentRef{
+						{
+							Attachment: &ParentRefAttachmentStatus{
+								Attached: true,
+							},
+						},
+					},
+					Valid:      true,
+					Attachable: true,
+				},
+			},
+
+			NGFPolicies: ngfPolicies,
+		}
+	}
+
+	newModifiedGraph := func(mod func(g *Graph) *Graph) *Graph {
+		return mod(newGraph())
+	}
+
+	expectNoPolicyAttachment := func(g *WithT, graph *Graph) {
+		if graph.Gateway != nil {
+			g.Expect(graph.Gateway.Policies).To(BeNil())
+		}
+
+		for _, r := range graph.Routes {
+			g.Expect(r.Policies).To(BeNil())
+		}
+	}
+
+	expectPolicyAttachment := func(g *WithT, graph *Graph) {
+		if graph.Gateway != nil {
+			g.Expect(graph.Gateway.Policies).To(HaveLen(1))
+		}
+
+		for _, r := range graph.Routes {
+			g.Expect(r.Policies).To(HaveLen(1))
+		}
+	}
+
+	expectGatewayPolicyAttachment := func(g *WithT, graph *Graph) {
+		if graph.Gateway != nil {
+			g.Expect(graph.Gateway.Policies).To(HaveLen(1))
+		}
+
+		for _, r := range graph.Routes {
+			g.Expect(r.Policies).To(BeNil())
+		}
+	}
+
+	tests := []struct {
+		graph  *Graph
+		expect func(g *WithT, graph *Graph)
+		name   string
+	}{
+		{
+			name: "nil Gateway",
+			graph: newModifiedGraph(func(g *Graph) *Graph {
+				g.Gateway = nil
+				return g
+			}),
+			expect: expectNoPolicyAttachment,
+		},
+		{
+			name: "nil routes",
+			graph: newModifiedGraph(func(g *Graph) *Graph {
+				g.Routes = nil
+				return g
+			}),
+			expect: expectGatewayPolicyAttachment,
+		},
+		{
+			name:   "normal",
+			graph:  newGraph(),
+			expect: expectPolicyAttachment,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			test.graph.attachPolicies("nginx-gateway")
+			test.expect(g, test.graph)
+		})
+	}
+}
+
+func TestAttachPolicyToRoute(t *testing.T) {
+	routeNsName := types.NamespacedName{Namespace: testNs, Name: "hr-route"}
+
+	createRoute := func(routeType RouteType, valid, attachable, parentRefs bool) *L7Route {
+		route := &L7Route{
+			Source: &v1.HTTPRoute{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      routeNsName.Name,
+					Namespace: routeNsName.Namespace,
+				},
+			},
+			Valid:      valid,
+			Attachable: attachable,
+			RouteType:  routeType,
+		}
+
+		if parentRefs {
+			route.ParentRefs = []ParentRef{
+				{
+					Attachment: &ParentRefAttachmentStatus{
+						Attached: true,
+					},
+				},
+			}
+		}
+
+		return route
+	}
+
+	createGRPCRoute := func(valid, attachable, parentRefs bool) *L7Route {
+		return createRoute(RouteTypeGRPC, valid, attachable, parentRefs)
+	}
+
+	createHTTPRoute := func(valid, attachable, parentRefs bool) *L7Route {
+		return createRoute(RouteTypeHTTP, valid, attachable, parentRefs)
+	}
+
+	createExpAncestor := func(kind v1.Kind) v1.ParentReference {
+		return v1.ParentReference{
+			Group:     helpers.GetPointer[v1.Group](v1.GroupName),
+			Kind:      helpers.GetPointer[v1.Kind](kind),
+			Namespace: (*v1.Namespace)(&routeNsName.Namespace),
+			Name:      v1.ObjectName(routeNsName.Name),
+		}
+	}
+
+	tests := []struct {
+		route       *L7Route
+		policy      *Policy
+		expAncestor *PolicyAncestor
+		name        string
+		expAttached bool
+	}{
+		{
+			name:   "policy attaches to http route",
+			route:  createHTTPRoute(true /*valid*/, true /*attachable*/, true /*parentRefs*/),
+			policy: &Policy{Source: &policiesfakes.FakePolicy{}},
+			expAncestor: &PolicyAncestor{
+				Ancestor: createExpAncestor(kinds.HTTPRoute),
+			},
+			expAttached: true,
+		},
+		{
+			name:   "policy attaches to grpc route",
+			route:  createGRPCRoute(true /*valid*/, true /*attachable*/, true /*parentRefs*/),
+			policy: &Policy{Source: &policiesfakes.FakePolicy{}},
+			expAncestor: &PolicyAncestor{
+				Ancestor: createExpAncestor(kinds.GRPCRoute),
+			},
+			expAttached: true,
+		},
+		{
+			name:   "no attachment; unattachable route",
+			route:  createHTTPRoute(true /*valid*/, false /*attachable*/, true /*parentRefs*/),
+			policy: &Policy{Source: &policiesfakes.FakePolicy{}},
+			expAncestor: &PolicyAncestor{
+				Ancestor:   createExpAncestor(kinds.HTTPRoute),
+				Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")},
+			},
+			expAttached: false,
+		},
+		{
+			name:   "no attachment; missing parentRefs",
+			route:  createHTTPRoute(true /*valid*/, true /*attachable*/, false /*parentRefs*/),
+			policy: &Policy{Source: &policiesfakes.FakePolicy{}},
+			expAncestor: &PolicyAncestor{
+				Ancestor:   createExpAncestor(kinds.HTTPRoute),
+				Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")},
+			},
+			expAttached: false,
+		},
+		{
+			name:   "no attachment; invalid route",
+			route:  createHTTPRoute(false /*valid*/, true /*attachable*/, true /*parentRefs*/),
+			policy: &Policy{Source: &policiesfakes.FakePolicy{}},
+			expAncestor: &PolicyAncestor{
+				Ancestor:   createExpAncestor(kinds.HTTPRoute),
+				Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")},
+			},
+			expAttached: false,
+		},
+		{
+			name:        "no attachment; max ancestors",
+			route:       createHTTPRoute(true /*valid*/, true /*attachable*/, true /*parentRefs*/),
+			policy:      &Policy{Source: createTestPolicyWithAncestors(16)},
+			expAncestor: nil,
+			expAttached: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			attachPolicyToRoute(test.policy, test.route, "nginx-gateway")
+
+			if test.expAttached {
+				g.Expect(test.route.Policies).To(HaveLen(1))
+			} else {
+				g.Expect(test.route.Policies).To(BeEmpty())
+			}
+
+			g.Expect(test.policy.Ancestor).To(BeEquivalentTo(test.expAncestor))
+		})
+	}
+}
+
+func TestAttachPolicyToGateway(t *testing.T) {
+	gatewayNsName := types.NamespacedName{Namespace: testNs, Name: "gateway"}
+	gateway2NsName := types.NamespacedName{Namespace: testNs, Name: "gateway2"}
+	ignoredGatewayNsName := types.NamespacedName{Namespace: testNs, Name: "ignored"}
+
+	newGateway := func(valid bool, nsname types.NamespacedName) *Gateway {
+		return &Gateway{
+			Source: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: nsname.Namespace,
+					Name:      nsname.Name,
+				},
+			},
+			Valid: valid,
+		}
+	}
+
+	getGatewayParentRef := func(gwNsName types.NamespacedName) v1.ParentReference {
+		return v1.ParentReference{
+			Group:     helpers.GetPointer[v1.Group](v1.GroupName),
+			Kind:      helpers.GetPointer[v1.Kind]("Gateway"),
+			Namespace: (*v1.Namespace)(&gwNsName.Namespace),
+			Name:      v1.ObjectName(gwNsName.Name),
+		}
+	}
+
+	tests := []struct {
+		policy      *Policy
+		gw          *Gateway
+		expAncestor *PolicyAncestor
+		name        string
+		expAttached bool
+	}{
+		{
+			name: "attached",
+			policy: &Policy{
+				Source: &policiesfakes.FakePolicy{},
+				TargetRef: PolicyTargetRef{
+					Nsname: gatewayNsName,
+					Kind:   "Gateway",
+				},
+			},
+			gw: newGateway(true, gatewayNsName),
+			expAncestor: &PolicyAncestor{
+				Ancestor: getGatewayParentRef(gatewayNsName),
+			},
+			expAttached: true,
+		},
+		{
+			name: "not attached; gateway ignored",
+			policy: &Policy{
+				Source: &policiesfakes.FakePolicy{},
+				TargetRef: PolicyTargetRef{
+					Nsname: ignoredGatewayNsName,
+					Kind:   "Gateway",
+				},
+			},
+			gw: newGateway(true, gatewayNsName),
+			expAncestor: &PolicyAncestor{
+				Ancestor:   getGatewayParentRef(ignoredGatewayNsName),
+				Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is ignored")},
+			},
+			expAttached: false,
+		},
+		{
+			name: "not attached; invalid gateway",
+			policy: &Policy{
+				Source: &policiesfakes.FakePolicy{},
+				TargetRef: PolicyTargetRef{
+					Nsname: gatewayNsName,
+					Kind:   "Gateway",
+				},
+			},
+			gw: newGateway(false, gatewayNsName),
+			expAncestor: &PolicyAncestor{
+				Ancestor:   getGatewayParentRef(gatewayNsName),
+				Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")},
+			},
+			expAttached: false,
+		},
+		{
+			name: "not attached; non-NGF gateway",
+			policy: &Policy{
+				Source: &policiesfakes.FakePolicy{},
+				TargetRef: PolicyTargetRef{
+					Nsname: gateway2NsName,
+					Kind:   "Gateway",
+				},
+			},
+			gw:          newGateway(true, gatewayNsName),
+			expAncestor: nil,
+			expAttached: false,
+		},
+		{
+			name: "not attached; max ancestors",
+			policy: &Policy{
+				Source: createTestPolicyWithAncestors(16),
+				TargetRef: PolicyTargetRef{
+					Nsname: gatewayNsName,
+					Kind:   "Gateway",
+				},
+			},
+			gw:          newGateway(true, gatewayNsName),
+			expAncestor: nil,
+			expAttached: false,
+		},
+	}
+
+	for _, test := range tests {
+		ignoredGateways := map[types.NamespacedName]*v1.Gateway{
+			ignoredGatewayNsName: nil,
+		}
+
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			attachPolicyToGateway(test.policy, test.gw, ignoredGateways, "nginx-gateway")
+
+			if test.expAttached {
+				g.Expect(test.gw.Policies).To(HaveLen(1))
+			} else {
+				g.Expect(test.gw.Policies).To(BeEmpty())
+			}
+
+			g.Expect(test.policy.Ancestor).To(BeEquivalentTo(test.expAncestor))
+		})
+	}
+}
+
+func TestProcessPolicies(t *testing.T) {
+	policyGVK := schema.GroupVersionKind{Group: "Group", Version: "Version", Kind: "MyPolicy"}
+
+	// These refs reference objects that belong to NGF.
+	// Policies that contain these refs should be processed.
+	hrRef := createTestRef(kinds.HTTPRoute, v1.GroupName, "hr")
+	grpcRef := createTestRef(kinds.GRPCRoute, v1.GroupName, "grpc")
+	gatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "gw")
+	ignoredGatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "ignored")
+
+	// These refs reference objects that do not belong to NGF.
+	// Policies that contain these refs should NOT be processed.
+	hrDoesNotExistRef := createTestRef(kinds.HTTPRoute, v1.GroupName, "dne")
+	hrWrongGroup := createTestRef(kinds.HTTPRoute, "WrongGroup", "hr")
+	gatewayWrongGroupRef := createTestRef(kinds.Gateway, "WrongGroup", "gw")
+	nonNGFGatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "not-ours")
+
+	pol1, pol1Key := createTestPolicyAndKey(policyGVK, hrRef, "pol1")
+	pol2, pol2Key := createTestPolicyAndKey(policyGVK, grpcRef, "pol2")
+	pol3, pol3Key := createTestPolicyAndKey(policyGVK, gatewayRef, "pol3")
+	pol4, pol4Key := createTestPolicyAndKey(policyGVK, ignoredGatewayRef, "pol4")
+	pol5, pol5Key := createTestPolicyAndKey(policyGVK, hrDoesNotExistRef, "pol5")
+	pol6, pol6Key := createTestPolicyAndKey(policyGVK, hrWrongGroup, "pol6")
+	pol7, pol7Key := createTestPolicyAndKey(policyGVK, gatewayWrongGroupRef, "pol7")
+	pol8, pol8Key := createTestPolicyAndKey(policyGVK, nonNGFGatewayRef, "pol8")
+
+	pol1Conflict, pol1ConflictKey := createTestPolicyAndKey(policyGVK, hrRef, "pol1-conflict")
+
+	allValidValidator := &policiesfakes.FakeValidator{}
+
+	tests := []struct {
+		validator            validation.PolicyValidator
+		policies             map[PolicyKey]policies.Policy
+		expProcessedPolicies map[PolicyKey]*Policy
+		name                 string
+	}{
+		{
+			name:                 "nil policies",
+			expProcessedPolicies: nil,
+		},
+		{
+			name:      "mix of relevant and irrelevant policies",
+			validator: allValidValidator,
+			policies: map[PolicyKey]policies.Policy{
+				pol1Key: pol1,
+				pol2Key: pol2,
+				pol3Key: pol3,
+				pol4Key: pol4,
+				pol5Key: pol5,
+				pol6Key: pol6,
+				pol7Key: pol7,
+				pol8Key: pol8,
+			},
+			expProcessedPolicies: map[PolicyKey]*Policy{
+				pol1Key: {
+					Source: pol1,
+					TargetRef: PolicyTargetRef{
+						Nsname: types.NamespacedName{Namespace: testNs, Name: "hr"},
+						Kind:   kinds.HTTPRoute,
+						Group:  v1.GroupName,
+					},
+					Valid: true,
+				},
+				pol2Key: {
+					Source: pol2,
+					TargetRef: PolicyTargetRef{
+						Nsname: types.NamespacedName{Namespace: testNs, Name: "grpc"},
+						Kind:   kinds.GRPCRoute,
+						Group:  v1.GroupName,
+					},
+					Valid: true,
+				},
+				pol3Key: {
+					Source: pol3,
+					TargetRef: PolicyTargetRef{
+						Nsname: types.NamespacedName{Namespace: testNs, Name: "gw"},
+						Kind:   kinds.Gateway,
+						Group:  v1.GroupName,
+					},
+					Valid: true,
+				},
+				pol4Key: {
+					Source: pol4,
+					TargetRef: PolicyTargetRef{
+						Nsname: types.NamespacedName{Namespace: testNs, Name: "ignored"},
+						Kind:   kinds.Gateway,
+						Group:  v1.GroupName,
+					},
+					Valid: true,
+				},
+			},
+		},
+		{
+			name: "invalid and valid policies",
+			validator: &policiesfakes.FakeValidator{
+				ValidateStub: func(policy policies.Policy) error {
+					if policy.GetName() == "pol1" {
+						return errors.New("invalid error")
+					}
+
+					return nil
+				},
+			},
+			policies: map[PolicyKey]policies.Policy{
+				pol1Key: pol1,
+				pol2Key: pol2,
+			},
+			expProcessedPolicies: map[PolicyKey]*Policy{
+				pol1Key: {
+					Source: pol1,
+					TargetRef: PolicyTargetRef{
+						Nsname: types.NamespacedName{Namespace: testNs, Name: "hr"},
+						Kind:   kinds.HTTPRoute,
+						Group:  v1.GroupName,
+					},
+					Conditions: []conditions.Condition{
+						staticConds.NewPolicyInvalid("invalid error"),
+					},
+					Valid: false,
+				},
+				pol2Key: {
+					Source: pol2,
+					TargetRef: PolicyTargetRef{
+						Nsname: types.NamespacedName{Namespace: testNs, Name: "grpc"},
+						Kind:   kinds.GRPCRoute,
+						Group:  v1.GroupName,
+					},
+					Valid: true,
+				},
+			},
+		},
+		{
+			name: "conflicted policies",
+			validator: &policiesfakes.FakeValidator{
+				ConflictsStub: func(_ policies.Policy, _ policies.Policy) bool {
+					return true
+				},
+			},
+			policies: map[PolicyKey]policies.Policy{
+				pol1Key:         pol1,
+				pol1ConflictKey: pol1Conflict,
+			},
+			expProcessedPolicies: map[PolicyKey]*Policy{
+				pol1Key: {
+					Source: pol1,
+					TargetRef: PolicyTargetRef{
+						Nsname: types.NamespacedName{Namespace: testNs, Name: "hr"},
+						Kind:   kinds.HTTPRoute,
+						Group:  v1.GroupName,
+					},
+					Valid: true,
+				},
+				pol1ConflictKey: {
+					Source: pol1Conflict,
+					TargetRef: PolicyTargetRef{
+						Nsname: types.NamespacedName{Namespace: testNs, Name: "hr"},
+						Kind:   kinds.HTTPRoute,
+						Group:  v1.GroupName,
+					},
+					Conditions: []conditions.Condition{
+						staticConds.NewPolicyConflicted("Conflicts with another MyPolicy"),
+					},
+					Valid: false,
+				},
+			},
+		},
+	}
+
+	gateways := processedGateways{
+		Winner: &v1.Gateway{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "gw",
+				Namespace: testNs,
+			},
+		},
+		Ignored: map[types.NamespacedName]*v1.Gateway{
+			{Namespace: testNs, Name: "ignored"}: {
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "gw",
+					Namespace: testNs,
+				},
+			},
+		},
+	}
+
+	routes := map[RouteKey]*L7Route{
+		{RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "hr"}}:   {},
+		{RouteType: RouteTypeGRPC, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "grpc"}}: {},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			processed := processPolicies(test.policies, test.validator, gateways, routes)
+			g.Expect(processed).To(BeEquivalentTo(test.expProcessedPolicies))
+		})
+	}
+}
+
+func TestMarkConflictedPolicies(t *testing.T) {
+	hrRef := createTestRef(kinds.HTTPRoute, v1.GroupName, "hr")
+	hrTargetRef := PolicyTargetRef{
+		Kind:   hrRef.Kind,
+		Group:  hrRef.Group,
+		Nsname: types.NamespacedName{Namespace: testNs, Name: string(hrRef.Name)},
+	}
+
+	grpcRef := createTestRef(kinds.GRPCRoute, v1.GroupName, "grpc")
+	grpcTargetRef := PolicyTargetRef{
+		Kind:   grpcRef.Kind,
+		Group:  grpcRef.Group,
+		Nsname: types.NamespacedName{Namespace: testNs, Name: string(grpcRef.Name)},
+	}
+
+	orangeGVK := schema.GroupVersionKind{Group: "Fruits", Version: "Fresh", Kind: "OrangePolicy"}
+	appleGVK := schema.GroupVersionKind{Group: "Fruits", Version: "Fresh", Kind: "ApplePolicy"}
+
+	tests := []struct {
+		name                  string
+		policies              map[PolicyKey]*Policy
+		fakeValidator         *policiesfakes.FakeValidator
+		conflictedNames       []string
+		expConflictToBeCalled bool
+	}{
+		{
+			name: "different policy types can not conflict",
+			policies: map[PolicyKey]*Policy{
+				createTestPolicyKey(orangeGVK, "orange"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "orange"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+				createTestPolicyKey(appleGVK, "apple"): {
+					Source:    createTestPolicy(appleGVK, hrRef, "apple"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+			},
+			fakeValidator:         &policiesfakes.FakeValidator{},
+			expConflictToBeCalled: false,
+		},
+		{
+			name: "policies of the same type but with different target refs can not conflict",
+			policies: map[PolicyKey]*Policy{
+				createTestPolicyKey(orangeGVK, "orange1"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "orange1"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+				createTestPolicyKey(orangeGVK, "orange2"): {
+					Source:    createTestPolicy(orangeGVK, grpcRef, "orange2"),
+					TargetRef: grpcTargetRef,
+					Valid:     true,
+				},
+			},
+			fakeValidator:         &policiesfakes.FakeValidator{},
+			expConflictToBeCalled: false,
+		},
+		{
+			name: "invalid policies can not conflict",
+			policies: map[PolicyKey]*Policy{
+				createTestPolicyKey(orangeGVK, "valid"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "valid"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+				createTestPolicyKey(orangeGVK, "invalid"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "invalid"),
+					TargetRef: hrTargetRef,
+					Valid:     false,
+				},
+			},
+			fakeValidator:         &policiesfakes.FakeValidator{},
+			expConflictToBeCalled: false,
+		},
+		{
+			name: "when a policy conflicts with a policy that has greater precedence it's marked as invalid and a" +
+				" condition is added",
+			policies: map[PolicyKey]*Policy{
+				createTestPolicyKey(orangeGVK, "orange1"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "orange1"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+				createTestPolicyKey(orangeGVK, "orange2"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "orange2"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+				createTestPolicyKey(orangeGVK, "orange3-conflicts-with-1"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "orange3-conflicts-with-1"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+				createTestPolicyKey(orangeGVK, "orange4"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "orange4"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+				createTestPolicyKey(orangeGVK, "orange5-conflicts-with-4"): {
+					Source:    createTestPolicy(orangeGVK, hrRef, "orange5-conflicts-with-4"),
+					TargetRef: hrTargetRef,
+					Valid:     true,
+				},
+			},
+			fakeValidator: &policiesfakes.FakeValidator{
+				ConflictsStub: func(policy policies.Policy, policy2 policies.Policy) bool {
+					pol1Name := policy.GetName()
+					pol2Name := policy2.GetName()
+
+					if pol1Name == "orange1" && pol2Name == "orange3-conflicts-with-1" {
+						return true
+					}
+
+					if pol1Name == "orange4" && pol2Name == "orange5-conflicts-with-4" {
+						return true
+					}
+
+					return false
+				},
+			},
+			conflictedNames:       []string{"orange3-conflicts-with-1", "orange5-conflicts-with-4"},
+			expConflictToBeCalled: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			markConflictedPolicies(test.policies, test.fakeValidator)
+
+			if !test.expConflictToBeCalled {
+				g.Expect(test.fakeValidator.ConflictsCallCount()).To(BeZero())
+			} else {
+				g.Expect(test.fakeValidator.ConflictsCallCount()).To(Not(BeZero()))
+				expConflictCond := staticConds.NewPolicyConflicted("Conflicts with another OrangePolicy")
+
+				for key, policy := range test.policies {
+					if slices.Contains(test.conflictedNames, key.NsName.Name) {
+						g.Expect(policy.Valid).To(BeFalse())
+						g.Expect(policy.Conditions).To(ConsistOf(expConflictCond))
+					} else {
+						g.Expect(policy.Valid).To(BeTrue())
+						g.Expect(policy.Conditions).To(BeEmpty())
+					}
+				}
+			}
+		})
+	}
+}
+
+func createTestPolicyWithAncestors(numAncestors int) policies.Policy {
+	policy := &policiesfakes.FakePolicy{}
+
+	ancestors := make([]v1alpha2.PolicyAncestorStatus, numAncestors)
+
+	for i := 0; i < numAncestors; i++ {
+		ancestors[i] = v1alpha2.PolicyAncestorStatus{ControllerName: "some-other-controller"}
+	}
+
+	policy.GetPolicyStatusReturns(v1alpha2.PolicyStatus{Ancestors: ancestors})
+	return policy
+}
+
+func createTestPolicyAndKey(
+	gvk schema.GroupVersionKind,
+	ref v1alpha2.LocalPolicyTargetReference,
+	name string,
+) (policies.Policy, PolicyKey) {
+	pol := createTestPolicy(gvk, ref, name)
+	key := createTestPolicyKey(gvk, name)
+
+	return pol, key
+}
+
+func createTestPolicy(
+	gvk schema.GroupVersionKind,
+	ref v1alpha2.LocalPolicyTargetReference,
+	name string,
+) policies.Policy {
+	return &policiesfakes.FakePolicy{
+		GetNameStub: func() string {
+			return name
+		},
+		GetNamespaceStub: func() string {
+			return testNs
+		},
+		GetTargetRefStub: func() v1alpha2.LocalPolicyTargetReference {
+			return ref
+		},
+		GetObjectKindStub: func() schema.ObjectKind {
+			return &policiesfakes.FakeObjectKind{
+				GroupVersionKindStub: func() schema.GroupVersionKind {
+					return gvk
+				},
+			}
+		},
+	}
+}
+
+func createTestPolicyKey(gvk schema.GroupVersionKind, name string) PolicyKey {
+	return PolicyKey{
+		NsName: types.NamespacedName{Namespace: testNs, Name: name},
+		GVK:    gvk,
+	}
+}
+
+func createTestRef(kind v1.Kind, group v1.Group, name string) v1alpha2.LocalPolicyTargetReference {
+	return v1alpha2.LocalPolicyTargetReference{
+		Group: group,
+		Kind:  kind,
+		Name:  v1.ObjectName(name),
+	}
+}
diff --git a/internal/mode/static/state/graph/policy_ancestor_test.go b/internal/mode/static/state/graph/policy_ancestor_test.go
new file mode 100644
index 00000000..114ce80d
--- /dev/null
+++ b/internal/mode/static/state/graph/policy_ancestor_test.go
@@ -0,0 +1,54 @@
+package graph
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+)
+
+func TestAncestorsFull(t *testing.T) {
+	createCurStatus := func(numAncestors int, ctlrName string) []v1alpha2.PolicyAncestorStatus {
+		statuses := make([]v1alpha2.PolicyAncestorStatus, 0, numAncestors)
+
+		for i := 0; i < numAncestors; i++ {
+			statuses = append(statuses, v1alpha2.PolicyAncestorStatus{
+				ControllerName: v1.GatewayController(ctlrName),
+			})
+		}
+
+		return statuses
+	}
+
+	tests := []struct {
+		name      string
+		curStatus []v1alpha2.PolicyAncestorStatus
+		expFull   bool
+	}{
+		{
+			name:      "not full",
+			curStatus: createCurStatus(15, "controller"),
+			expFull:   false,
+		},
+		{
+			name:      "full; ancestor does not exist in current status",
+			curStatus: createCurStatus(16, "controller"),
+			expFull:   true,
+		},
+		{
+			name:      "full, but ancestor does exist in current status",
+			curStatus: createCurStatus(16, "nginx-gateway"),
+			expFull:   false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			full := ancestorsFull(test.curStatus, "nginx-gateway")
+			g.Expect(full).To(Equal(test.expFull))
+		})
+	}
+}
diff --git a/internal/mode/static/state/graph/reference_grant_test.go b/internal/mode/static/state/graph/reference_grant_test.go
index aa84446b..3b61e6f1 100644
--- a/internal/mode/static/state/graph/reference_grant_test.go
+++ b/internal/mode/static/state/graph/reference_grant_test.go
@@ -8,6 +8,7 @@ import (
 	"sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 )
 
 func TestReferenceGrantResolver(t *testing.T) {
@@ -20,7 +21,7 @@ func TestReferenceGrantResolver(t *testing.T) {
 				From: []v1beta1.ReferenceGrantFrom{
 					{
 						Group:     v1beta1.GroupName,
-						Kind:      "Gateway",
+						Kind:      kinds.Gateway,
 						Namespace: v1beta1.Namespace(gwNs),
 					},
 				},
@@ -45,7 +46,7 @@ func TestReferenceGrantResolver(t *testing.T) {
 				From: []v1beta1.ReferenceGrantFrom{
 					{
 						Group:     v1beta1.GroupName,
-						Kind:      "Gateway",
+						Kind:      kinds.Gateway,
 						Namespace: v1beta1.Namespace(gwNs),
 					},
 				},
@@ -68,17 +69,17 @@ func TestReferenceGrantResolver(t *testing.T) {
 				From: []v1beta1.ReferenceGrantFrom{
 					{
 						Group:     v1beta1.GroupName,
-						Kind:      "Gateway",
+						Kind:      kinds.Gateway,
 						Namespace: "wrong-ns1",
 					},
 					{
 						Group:     v1beta1.GroupName,
-						Kind:      "Gateway",
+						Kind:      kinds.Gateway,
 						Namespace: "wrong-ns2",
 					},
 					{
 						Group:     v1beta1.GroupName,
-						Kind:      "Gateway",
+						Kind:      kinds.Gateway,
 						Namespace: v1beta1.Namespace(gwNs),
 					},
 				},
@@ -87,7 +88,7 @@ func TestReferenceGrantResolver(t *testing.T) {
 	}
 
 	normalTo := toResource{kind: "Secret", name: secretNsName.Name, namespace: secretNsName.Namespace}
-	normalFrom := fromResource{group: v1beta1.GroupName, kind: "Gateway", namespace: gwNs}
+	normalFrom := fromResource{group: v1beta1.GroupName, kind: kinds.Gateway, namespace: gwNs}
 
 	tests := []struct {
 		to      toResource
@@ -127,13 +128,13 @@ func TestReferenceGrantResolver(t *testing.T) {
 		{
 			msg:     "wrong 'from' group",
 			to:      normalTo,
-			from:    fromResource{group: "wrong.group", kind: "Gateway", namespace: gwNs},
+			from:    fromResource{group: "wrong.group", kind: kinds.Gateway, namespace: gwNs},
 			allowed: false,
 		},
 		{
 			msg:     "wrong 'from' namespace",
 			to:      normalTo,
-			from:    fromResource{group: v1beta1.GroupName, kind: "Gateway", namespace: "wrong-ns"},
+			from:    fromResource{group: v1beta1.GroupName, kind: kinds.Gateway, namespace: "wrong-ns"},
 			allowed: false,
 		},
 		{
@@ -198,7 +199,7 @@ func TestFromGateway(t *testing.T) {
 
 	exp := fromResource{
 		group:     v1beta1.GroupName,
-		kind:      "Gateway",
+		kind:      kinds.Gateway,
 		namespace: "ns",
 	}
 
@@ -211,7 +212,7 @@ func TestFromHTTPRoute(t *testing.T) {
 
 	exp := fromResource{
 		group:     v1beta1.GroupName,
-		kind:      "HTTPRoute",
+		kind:      kinds.HTTPRoute,
 		namespace: "ns",
 	}
 
diff --git a/internal/mode/static/state/graph/route_common_test.go b/internal/mode/static/state/graph/route_common_test.go
index 85d5802b..3eb96f6f 100644
--- a/internal/mode/static/state/graph/route_common_test.go
+++ b/internal/mode/static/state/graph/route_common_test.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes"
 )
@@ -156,7 +157,7 @@ func TestFindGatewayForParentRef(t *testing.T) {
 		{
 			ref: gatewayv1.ParentReference{
 				Group:     helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
-				Kind:      helpers.GetPointer[gatewayv1.Kind]("Gateway"),
+				Kind:      helpers.GetPointer[gatewayv1.Kind](kinds.Gateway),
 				Namespace: helpers.GetPointer(gatewayv1.Namespace(gwNsName1.Namespace)),
 				Name:      gatewayv1.ObjectName(gwNsName1.Name),
 			},
@@ -1565,3 +1566,21 @@ func TestValidateFilterResponseHeaderModifier(t *testing.T) {
 		})
 	}
 }
+
+func TestRouteKeyForKind(t *testing.T) {
+	nsname := types.NamespacedName{Namespace: testNs, Name: "route"}
+
+	g := NewWithT(t)
+
+	key := routeKeyForKind(kinds.HTTPRoute, nsname)
+	g.Expect(key).To(Equal(RouteKey{RouteType: RouteTypeHTTP, NamespacedName: nsname}))
+
+	key = routeKeyForKind(kinds.GRPCRoute, nsname)
+	g.Expect(key).To(Equal(RouteKey{RouteType: RouteTypeGRPC, NamespacedName: nsname}))
+
+	rk := func() {
+		_ = routeKeyForKind(kinds.Gateway, nsname)
+	}
+
+	g.Expect(rk).To(Panic())
+}
diff --git a/internal/mode/static/status/prepare_requests_test.go b/internal/mode/static/status/prepare_requests_test.go
index 594e317d..546b534e 100644
--- a/internal/mode/static/status/prepare_requests_test.go
+++ b/internal/mode/static/status/prepare_requests_test.go
@@ -8,6 +8,7 @@ import (
 	. "github.com/onsi/gomega"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -20,6 +21,7 @@ import (
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
 	statusFramework "github.com/nginxinc/nginx-gateway-fabric/internal/framework/status"
 	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
@@ -1058,7 +1060,13 @@ func TestBuildGatewayStatuses(t *testing.T) {
 
 			updater := statusFramework.NewUpdater(k8sClient, zap.New())
 
-			reqs := PrepareGatewayRequests(test.gateway, test.ignoredGateways, transitionTime, addr, test.nginxReloadRes)
+			reqs := PrepareGatewayRequests(
+				test.gateway,
+				test.ignoredGateways,
+				transitionTime,
+				addr,
+				test.nginxReloadRes,
+			)
 
 			g.Expect(reqs).To(HaveLen(expectedTotalReqs))
 
@@ -1105,8 +1113,8 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) {
 		}
 	}
 
-	attachedConds := []conditions.Condition{staticConds.NewBackendTLSPolicyAccepted()}
-	invalidConds := []conditions.Condition{staticConds.NewBackendTLSPolicyInvalid("invalid backendTLSPolicy")}
+	attachedConds := []conditions.Condition{staticConds.NewPolicyAccepted()}
+	invalidConds := []conditions.Condition{staticConds.NewPolicyInvalid("invalid backendTLSPolicy")}
 
 	validPolicyCfg := policyCfg{
 		Name:         "valid-bt",
@@ -1156,6 +1164,8 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) {
 							AncestorRef: v1.ParentReference{
 								Namespace: helpers.GetPointer[v1.Namespace]("test"),
 								Name:      "gateway",
+								Group:     helpers.GetPointer[v1.Group](v1.GroupName),
+								Kind:      helpers.GetPointer[v1.Kind](kinds.Gateway),
 							},
 							ControllerName: gatewayCtlrName,
 							Conditions: []metav1.Condition{
@@ -1165,7 +1175,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) {
 									ObservedGeneration: 1,
 									LastTransitionTime: transitionTime,
 									Reason:             string(v1alpha2.PolicyReasonAccepted),
-									Message:            "BackendTLSPolicy is accepted by the Gateway",
+									Message:            "Policy is accepted",
 								},
 							},
 						},
@@ -1186,6 +1196,8 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) {
 							AncestorRef: v1.ParentReference{
 								Namespace: helpers.GetPointer[v1.Namespace]("test"),
 								Name:      "gateway",
+								Group:     helpers.GetPointer[v1.Group](v1.GroupName),
+								Kind:      helpers.GetPointer[v1.Kind](kinds.Gateway),
 							},
 							ControllerName: gatewayCtlrName,
 							Conditions: []metav1.Condition{
@@ -1230,6 +1242,8 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) {
 							AncestorRef: v1.ParentReference{
 								Namespace: helpers.GetPointer[v1.Namespace]("test"),
 								Name:      "gateway",
+								Group:     helpers.GetPointer[v1.Group](v1.GroupName),
+								Kind:      helpers.GetPointer[v1.Kind](kinds.Gateway),
 							},
 							ControllerName: gatewayCtlrName,
 							Conditions: []metav1.Condition{
@@ -1239,7 +1253,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) {
 									ObservedGeneration: 1,
 									LastTransitionTime: transitionTime,
 									Reason:             string(v1alpha2.PolicyReasonAccepted),
-									Message:            "BackendTLSPolicy is accepted by the Gateway",
+									Message:            "Policy is accepted",
 								},
 							},
 						},
@@ -1371,3 +1385,244 @@ func TestBuildNginxGatewayStatus(t *testing.T) {
 		})
 	}
 }
+
+func TestBuildNGFPolicyStatuses(t *testing.T) {
+	const gatewayCtlrName = "controller"
+
+	transitionTime := helpers.PrepareTimeForFakeClient(metav1.Now())
+
+	type policyCfg struct {
+		Ancestor   *graph.PolicyAncestor
+		Name       string
+		Conditions []conditions.Condition
+	}
+
+	// We have to use a real policy here because the test makes the status update using the k8sClient.
+	// One policy type should suffice here, unless a new policy introduces branching.
+	getPolicy := func(cfg policyCfg) *graph.Policy {
+		return &graph.Policy{
+			Source: &ngfAPI.ClientSettingsPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:       cfg.Name,
+					Namespace:  "test",
+					Generation: 2,
+				},
+			},
+			Conditions: cfg.Conditions,
+			Ancestor:   cfg.Ancestor,
+		}
+	}
+
+	invalidConds := []conditions.Condition{staticConds.NewPolicyInvalid("invalid")}
+	targetRefNotFoundConds := []conditions.Condition{staticConds.NewPolicyTargetNotFound("target not found")}
+
+	validPolicyKey := graph.PolicyKey{
+		NsName: types.NamespacedName{Namespace: "test", Name: "valid-pol"},
+		GVK:    schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy},
+	}
+	validPolicyCfg := policyCfg{
+		Name: validPolicyKey.NsName.Name,
+		Ancestor: &graph.PolicyAncestor{
+			Ancestor: v1.ParentReference{
+				Name: "ancestor",
+			},
+		},
+	}
+
+	invalidPolicyKey := graph.PolicyKey{
+		NsName: types.NamespacedName{Namespace: "test", Name: "invalid-pol"},
+		GVK:    schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy},
+	}
+	invalidPolicyCfg := policyCfg{
+		Name:       invalidPolicyKey.NsName.Name,
+		Conditions: invalidConds,
+		Ancestor: &graph.PolicyAncestor{
+			Ancestor: v1.ParentReference{
+				Name: "ancestor",
+			},
+		},
+	}
+
+	targetRefNotFoundPolicyKey := graph.PolicyKey{
+		NsName: types.NamespacedName{Namespace: "test", Name: "target-not-found-pol"},
+		GVK:    schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy},
+	}
+	targetRefNotFoundPolicyCfg := policyCfg{
+		Name: targetRefNotFoundPolicyKey.NsName.Name,
+		Ancestor: &graph.PolicyAncestor{
+			Ancestor: v1.ParentReference{
+				Name: "ancestor",
+			},
+			Conditions: targetRefNotFoundConds,
+		},
+	}
+
+	multiInvalidCondsPolicyKey := graph.PolicyKey{
+		NsName: types.NamespacedName{Namespace: "test", Name: "multi-invalid-conds-pol"},
+		GVK:    schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy},
+	}
+	multiInvalidCondsPolicyCfg := policyCfg{
+		Name:       multiInvalidCondsPolicyKey.NsName.Name,
+		Conditions: invalidConds,
+		Ancestor: &graph.PolicyAncestor{
+			Ancestor: v1.ParentReference{
+				Name: "ancestor",
+			},
+			Conditions: targetRefNotFoundConds,
+		},
+	}
+
+	nilAncestorPolicyKey := graph.PolicyKey{
+		NsName: types.NamespacedName{Namespace: "test", Name: "nil-ancestor-pol"},
+		GVK:    schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy},
+	}
+	nilAncestorPolicyCfg := policyCfg{
+		Name:     nilAncestorPolicyKey.NsName.Name,
+		Ancestor: nil,
+	}
+
+	tests := []struct {
+		policies map[graph.PolicyKey]*graph.Policy
+		expected map[types.NamespacedName]v1alpha2.PolicyStatus
+		name     string
+	}{
+		{
+			name:     "nil policies",
+			expected: map[types.NamespacedName]v1alpha2.PolicyStatus{},
+		},
+		{
+			name: "mix valid and invalid policies",
+			policies: map[graph.PolicyKey]*graph.Policy{
+				invalidPolicyKey:           getPolicy(invalidPolicyCfg),
+				targetRefNotFoundPolicyKey: getPolicy(targetRefNotFoundPolicyCfg),
+				validPolicyKey:             getPolicy(validPolicyCfg),
+			},
+			expected: map[types.NamespacedName]v1alpha2.PolicyStatus{
+				invalidPolicyKey.NsName: {
+					Ancestors: []v1alpha2.PolicyAncestorStatus{
+						{
+							AncestorRef: v1.ParentReference{
+								Name: "ancestor",
+							},
+							ControllerName: gatewayCtlrName,
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(v1alpha2.PolicyConditionAccepted),
+									Status:             metav1.ConditionFalse,
+									ObservedGeneration: 2,
+									LastTransitionTime: transitionTime,
+									Reason:             string(v1alpha2.PolicyReasonInvalid),
+									Message:            "invalid",
+								},
+							},
+						},
+					},
+				},
+				targetRefNotFoundPolicyKey.NsName: {
+					Ancestors: []v1alpha2.PolicyAncestorStatus{
+						{
+							AncestorRef: v1.ParentReference{
+								Name: "ancestor",
+							},
+							ControllerName: gatewayCtlrName,
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(v1alpha2.PolicyConditionAccepted),
+									Status:             metav1.ConditionFalse,
+									ObservedGeneration: 2,
+									LastTransitionTime: transitionTime,
+									Reason:             string(v1alpha2.PolicyReasonTargetNotFound),
+									Message:            "target not found",
+								},
+							},
+						},
+					},
+				},
+				validPolicyKey.NsName: {
+					Ancestors: []v1alpha2.PolicyAncestorStatus{
+						{
+							AncestorRef: v1.ParentReference{
+								Name: "ancestor",
+							},
+							ControllerName: gatewayCtlrName,
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(v1alpha2.PolicyConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									ObservedGeneration: 2,
+									LastTransitionTime: transitionTime,
+									Reason:             string(v1alpha2.PolicyReasonAccepted),
+									Message:            "Policy is accepted",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "policy with policy conditions and ancestor conditions; policy conditions win",
+			policies: map[graph.PolicyKey]*graph.Policy{
+				multiInvalidCondsPolicyKey: getPolicy(multiInvalidCondsPolicyCfg),
+			},
+			expected: map[types.NamespacedName]v1alpha2.PolicyStatus{
+				multiInvalidCondsPolicyKey.NsName: {
+					Ancestors: []v1alpha2.PolicyAncestorStatus{
+						{
+							AncestorRef: v1.ParentReference{
+								Name: "ancestor",
+							},
+							ControllerName: gatewayCtlrName,
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(v1alpha2.PolicyConditionAccepted),
+									Status:             metav1.ConditionFalse,
+									ObservedGeneration: 2,
+									LastTransitionTime: transitionTime,
+									Reason:             string(v1alpha2.PolicyReasonInvalid),
+									Message:            "invalid",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Policy with nil ancestor",
+			policies: map[graph.PolicyKey]*graph.Policy{
+				nilAncestorPolicyKey: getPolicy(nilAncestorPolicyCfg),
+			},
+			expected: map[types.NamespacedName]v1alpha2.PolicyStatus{},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			k8sClient := createK8sClientFor(&ngfAPI.ClientSettingsPolicy{})
+
+			for _, pol := range test.policies {
+				err := k8sClient.Create(context.Background(), pol.Source)
+				g.Expect(err).ToNot(HaveOccurred())
+			}
+
+			updater := statusFramework.NewUpdater(k8sClient, zap.New())
+
+			reqs := PrepareNGFPolicyRequests(test.policies, transitionTime, gatewayCtlrName)
+
+			g.Expect(reqs).To(HaveLen(len(test.expected)))
+
+			updater.Update(context.Background(), reqs...)
+
+			for nsname, expected := range test.expected {
+				var pol ngfAPI.ClientSettingsPolicy
+
+				err := k8sClient.Get(context.Background(), nsname, &pol)
+				g.Expect(err).ToNot(HaveOccurred())
+				g.Expect(helpers.Diff(expected, pol.Status)).To(BeEmpty())
+			}
+		})
+	}
+}
diff --git a/internal/mode/static/status/status_setters_test.go b/internal/mode/static/status/status_setters_test.go
index d258ec6a..a7b83834 100644
--- a/internal/mode/static/status/status_setters_test.go
+++ b/internal/mode/static/status/status_setters_test.go
@@ -11,6 +11,8 @@ import (
 
 	ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes"
 )
 
 func TestNewNginxGatewayStatusSetter(t *testing.T) {
@@ -665,6 +667,153 @@ func TestNewBackendTLSPolicyStatusSetter(t *testing.T) {
 	}
 }
 
+func TestNewNGFPolicyStatusSetter(t *testing.T) {
+	const (
+		controllerName      = "controller"
+		otherControllerName = "other-controller"
+	)
+
+	tests := []struct {
+		name                         string
+		status, newStatus, expStatus v1alpha2.PolicyStatus
+		expStatusSet                 bool
+	}{
+		{
+			name: "Policy has no status",
+			newStatus: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "new condition"}},
+					},
+				},
+			},
+			expStatus: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "new condition"}},
+					},
+				},
+			},
+			expStatusSet: true,
+		},
+		{
+			name: "Policy has old status",
+			newStatus: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "new condition"}},
+					},
+				},
+			},
+			status: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "old condition"}},
+					},
+				},
+			},
+			expStatus: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "new condition"}},
+					},
+				},
+			},
+			expStatusSet: true,
+		},
+		{
+			name: "Policy has old status and other controller status",
+			newStatus: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "new condition"}},
+					},
+				},
+			},
+			status: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "old condition"}},
+					},
+					{
+						ControllerName: otherControllerName,
+						Conditions:     []metav1.Condition{{Message: "some condition"}},
+					},
+				},
+			},
+			expStatus: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: otherControllerName,
+						Conditions:     []metav1.Condition{{Message: "some condition"}},
+					},
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "new condition"}},
+					},
+				},
+			},
+			expStatusSet: true,
+		},
+		{
+			name: "Policy has same status",
+			newStatus: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "same condition"}},
+					},
+				},
+			},
+			status: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "same condition"}},
+					},
+				},
+			},
+			expStatus: v1alpha2.PolicyStatus{
+				Ancestors: []v1alpha2.PolicyAncestorStatus{
+					{
+						ControllerName: controllerName,
+						Conditions:     []metav1.Condition{{Message: "same condition"}},
+					},
+				},
+			},
+			expStatusSet: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			setter := newNGFPolicyStatusSetter(test.newStatus, controllerName)
+			obj := &policiesfakes.FakePolicy{
+				GetPolicyStatusStub: func() v1alpha2.PolicyStatus {
+					return test.status
+				},
+			}
+
+			statusSet := setter(obj)
+
+			g.Expect(statusSet).To(Equal(test.expStatusSet))
+
+			if statusSet {
+				g.Expect(obj.SetPolicyStatusArgsForCall(0)).To(Equal(test.expStatus))
+			}
+		})
+	}
+}
+
 func TestGWStatusEqual(t *testing.T) {
 	getDefaultStatus := func() gatewayv1.GatewayStatus {
 		return gatewayv1.GatewayStatus{
@@ -689,7 +838,7 @@ func TestGWStatusEqual(t *testing.T) {
 					SupportedKinds: []gatewayv1.RouteGroupKind{
 						{
 							Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
-							Kind:  "HTTPRoute",
+							Kind:  kinds.HTTPRoute,
 						},
 						{
 							Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
@@ -708,7 +857,7 @@ func TestGWStatusEqual(t *testing.T) {
 					SupportedKinds: []gatewayv1.RouteGroupKind{
 						{
 							Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
-							Kind:  "HTTPRoute",
+							Kind:  kinds.HTTPRoute,
 						},
 					},
 					AttachedRoutes: 1,
@@ -723,7 +872,7 @@ func TestGWStatusEqual(t *testing.T) {
 					SupportedKinds: []gatewayv1.RouteGroupKind{
 						{
 							Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
-							Kind:  "HTTPRoute",
+							Kind:  kinds.HTTPRoute,
 						},
 					},
 					AttachedRoutes: 1,
@@ -1091,87 +1240,51 @@ func TestRouteParentStatusEqual(t *testing.T) {
 	}
 }
 
-func TestEqualPointers(t *testing.T) {
-	tests := []struct {
-		p1       *string
-		p2       *string
-		name     string
-		expEqual bool
-	}{
-		{
-			name:     "first pointer nil; second has non-empty value",
-			p1:       nil,
-			p2:       helpers.GetPointer("test"),
-			expEqual: false,
-		},
-		{
-			name:     "second pointer nil; first has non-empty value",
-			p1:       helpers.GetPointer("test"),
-			p2:       nil,
-			expEqual: false,
-		},
-		{
-			name:     "different values",
-			p1:       helpers.GetPointer("test"),
-			p2:       helpers.GetPointer("different"),
-			expEqual: false,
-		},
-		{
-			name:     "both pointers nil",
-			p1:       nil,
-			p2:       nil,
-			expEqual: true,
-		},
-		{
-			name:     "first pointer nil; second empty",
-			p1:       nil,
-			p2:       helpers.GetPointer(""),
-			expEqual: true,
-		},
-		{
-			name:     "second pointer nil; first empty",
-			p1:       helpers.GetPointer(""),
-			p2:       nil,
-			expEqual: true,
-		},
-		{
-			name:     "same value",
-			p1:       helpers.GetPointer("test"),
-			p2:       helpers.GetPointer("test"),
-			expEqual: true,
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			val := equalPointers(test.p1, test.p2)
-			g.Expect(val).To(Equal(test.expEqual))
-		})
-	}
-}
-
-func TestBtpStatusEqual(t *testing.T) {
-	getPolicyStatus := func(ancestorName, ancestorNs, ctlrName string) v1alpha2.PolicyStatus {
+func TestPolicyStatusEqual(t *testing.T) {
+	getPolicyStatus := func() v1alpha2.PolicyStatus {
 		return v1alpha2.PolicyStatus{
 			Ancestors: []v1alpha2.PolicyAncestorStatus{
 				{
 					AncestorRef: gatewayv1.ParentReference{
-						Namespace: helpers.GetPointer[gatewayv1.Namespace]((gatewayv1.Namespace)(ancestorNs)),
-						Name:      v1alpha2.ObjectName(ancestorName),
+						Namespace: helpers.GetPointer[gatewayv1.Namespace]("ns1"),
+						Name:      "ancestor1",
+						Group:     helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName),
+						Kind:      helpers.GetPointer[gatewayv1.Kind](kinds.Gateway),
 					},
-					ControllerName: v1alpha2.GatewayController(ctlrName),
+					ControllerName: "ctlr1",
 					Conditions:     []metav1.Condition{{Type: "otherType", Status: "otherStatus"}},
 				},
 			},
 		}
 	}
-	prevMultiple := getPolicyStatus("ancestor1", "ns1", "ctlr1")
-	prevMultiple.Ancestors = append(prevMultiple.Ancestors, getPolicyStatus("ancestor2", "ns2", "ctlr2").Ancestors...)
 
-	currMultiple := getPolicyStatus("ancestor1", "ns1", "ctlr1")
-	currMultiple.Ancestors = append(currMultiple.Ancestors, getPolicyStatus("ancestor3", "ns3", "ctlr2").Ancestors...)
+	type modFunc func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus
+
+	getModifiedPolicyStatus := func(mod modFunc) v1alpha2.PolicyStatus {
+		return mod(getPolicyStatus())
+	}
+
+	prevMultiple := getPolicyStatus()
+	prevMultiple.Ancestors = append(
+		prevMultiple.Ancestors,
+		getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+			ns := "ns2"
+			s.Ancestors[0].AncestorRef.Name = "ancestor2"
+			s.Ancestors[0].AncestorRef.Namespace = (*gatewayv1.Namespace)(&ns)
+			s.Ancestors[0].ControllerName = "ctlr2"
+			return s
+		}).Ancestors...)
+
+	currMultiple := getPolicyStatus()
+	currMultiple.Ancestors = append(
+		currMultiple.Ancestors,
+		getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+			ns := "ns3"
+			s.Ancestors[0].AncestorRef.Name = "ancestor3"
+			s.Ancestors[0].AncestorRef.Namespace = (*gatewayv1.Namespace)(&ns)
+			s.Ancestors[0].ControllerName = "ctlr3"
+			return s
+		}).Ancestors...)
 
 	tests := []struct {
 		name           string
@@ -1182,36 +1295,79 @@ func TestBtpStatusEqual(t *testing.T) {
 	}{
 		{
 			name:           "status equal",
-			previous:       getPolicyStatus("ancestor1", "ns1", "ctlr1"),
-			current:        getPolicyStatus("ancestor1", "ns1", "ctlr1"),
+			previous:       getPolicyStatus(),
+			current:        getPolicyStatus(),
 			controllerName: "ctlr1",
 			expEqual:       true,
 		},
 		{
-			name:           "status not equal, different ancestor name",
-			previous:       getPolicyStatus("ancestor1", "ns1", "ctlr1"),
-			current:        getPolicyStatus("ancestor2", "ns1", "ctlr1"),
+			name:     "status not equal, different ancestor name",
+			previous: getPolicyStatus(),
+			current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+				s.Ancestors[0].AncestorRef.Name = "diff"
+				return s
+			}),
+			controllerName: "ctlr1",
+			expEqual:       false,
+		},
+		{
+			name:     "status not equal, different ancestor namespace",
+			previous: getPolicyStatus(),
+			current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+				ns := "diff"
+				s.Ancestors[0].AncestorRef.Namespace = (*gatewayv1.Namespace)(&ns)
+				return s
+			}),
+			controllerName: "ctlr1",
+			expEqual:       false,
+		},
+		{
+			name:     "status not equal, different ancestor kind",
+			previous: getPolicyStatus(),
+			current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+				s.Ancestors[0].AncestorRef.Kind = helpers.GetPointer[gatewayv1.Kind]("diff")
+				return s
+			}),
+			controllerName: "ctlr1",
+			expEqual:       false,
+		},
+		{
+			name:     "status not equal, different ancestor group",
+			previous: getPolicyStatus(),
+			current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+				s.Ancestors[0].AncestorRef.Group = helpers.GetPointer[gatewayv1.Group]("diff")
+				return s
+			}),
 			controllerName: "ctlr1",
 			expEqual:       false,
 		},
 		{
-			name:           "status not equal, different ancestor namespace",
-			previous:       getPolicyStatus("ancestor1", "ns1", "ctlr1"),
-			current:        getPolicyStatus("ancestor1", "ns2", "ctlr1"),
+			name:     "status not equal, different controller name on current",
+			previous: getPolicyStatus(),
+			current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+				s.Ancestors[0].ControllerName = "diff"
+				return s
+			}),
 			controllerName: "ctlr1",
 			expEqual:       false,
 		},
 		{
-			name:           "status not equal, different controller name on current",
-			previous:       getPolicyStatus("ancestor1", "ns1", "ctlr1"),
-			current:        getPolicyStatus("ancestor1", "ns1", "ctlr2"),
+			name:     "status not equal, different conds",
+			previous: getPolicyStatus(),
+			current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+				s.Ancestors[0].Conditions = nil
+				return s
+			}),
 			controllerName: "ctlr1",
 			expEqual:       false,
 		},
 		{
-			name:           "status not equal, different controller name on previous",
-			previous:       getPolicyStatus("ancestor1", "ns1", "ctlr2"),
-			current:        getPolicyStatus("ancestor1", "ns1", "ctlr1"),
+			name: "status not equal, different controller name on previous",
+			previous: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus {
+				s.Ancestors[0].ControllerName = "diff"
+				return s
+			}),
+			current:        getPolicyStatus(),
 			controllerName: "ctlr1",
 			expEqual:       false,
 		},
@@ -1227,7 +1383,7 @@ func TestBtpStatusEqual(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			g := NewWithT(t)
-			equal := btpStatusEqual(test.controllerName, test.previous, test.current)
+			equal := policyStatusEqual(test.controllerName, test.previous, test.current)
 			g.Expect(equal).To(Equal(test.expEqual))
 		})
 	}