diff --git a/charts/nginx-gateway-fabric/templates/clusterrole.yaml b/charts/nginx-gateway-fabric/templates/clusterrole.yaml
index cbb163ae1d..9ee1be4254 100644
--- a/charts/nginx-gateway-fabric/templates/clusterrole.yaml
+++ b/charts/nginx-gateway-fabric/templates/clusterrole.yaml
@@ -104,6 +104,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   {{- if .Values.nginxGateway.snippetsFilters.enable }}
   - snippetsfilters
   {{- end }}
@@ -116,6 +117,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   {{- if .Values.nginxGateway.snippetsFilters.enable }}
   - snippetsfilters/status
   {{- end }}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 0128a189cb..b067d64141 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -5,3 +5,5 @@ resources:
   - bases/gateway.nginx.org_nginxgateways.yaml
   - bases/gateway.nginx.org_nginxproxies.yaml
   - bases/gateway.nginx.org_observabilitypolicies.yaml
+  - bases/gateway.nginx.org_snippetsfilters.yaml
+  - bases/gateway.nginx.org_upstreamsettingspolicies.yaml
diff --git a/deploy/aws-nlb/deploy.yaml b/deploy/aws-nlb/deploy.yaml
index 4f367638b5..0131c0a534 100644
--- a/deploy/aws-nlb/deploy.yaml
+++ b/deploy/aws-nlb/deploy.yaml
@@ -98,6 +98,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   verbs:
   - list
   - watch
@@ -107,6 +108,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   verbs:
   - update
 - apiGroups:
diff --git a/deploy/azure/deploy.yaml b/deploy/azure/deploy.yaml
index f4916da3ff..4073472171 100644
--- a/deploy/azure/deploy.yaml
+++ b/deploy/azure/deploy.yaml
@@ -98,6 +98,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   verbs:
   - list
   - watch
@@ -107,6 +108,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   verbs:
   - update
 - apiGroups:
diff --git a/deploy/crds.yaml b/deploy/crds.yaml
index 1e87d74771..73cb27daf3 100644
--- a/deploy/crds.yaml
+++ b/deploy/crds.yaml
@@ -1292,3 +1292,636 @@ spec:
     storage: true
     subresources:
       status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.16.5
+  name: snippetsfilters.gateway.nginx.org
+spec:
+  group: gateway.nginx.org
+  names:
+    categories:
+    - nginx-gateway-fabric
+    kind: SnippetsFilter
+    listKind: SnippetsFilterList
+    plural: snippetsfilters
+    shortNames:
+    - snippetsfilter
+    singular: snippetsfilter
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: |-
+          SnippetsFilter is a filter that allows inserting NGINX configuration into the
+          generated NGINX config for HTTPRoute and GRPCRoute resources.
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: Spec defines the desired state of the SnippetsFilter.
+            properties:
+              snippets:
+                description: |-
+                  Snippets is a list of NGINX configuration snippets.
+                  There can only be one snippet per context.
+                  Allowed contexts: main, http, http.server, http.server.location.
+                items:
+                  description: Snippet represents an NGINX configuration snippet.
+                  properties:
+                    context:
+                      description: Context is the NGINX context to insert the snippet
+                        into.
+                      enum:
+                      - main
+                      - http
+                      - http.server
+                      - http.server.location
+                      type: string
+                    value:
+                      description: Value is the NGINX configuration snippet.
+                      minLength: 1
+                      type: string
+                  required:
+                  - context
+                  - value
+                  type: object
+                maxItems: 4
+                minItems: 1
+                type: array
+                x-kubernetes-validations:
+                - message: Only one snippet allowed per context
+                  rule: self.all(s1, self.exists_one(s2, s1.context == s2.context))
+            required:
+            - snippets
+            type: object
+          status:
+            description: Status defines the state of the SnippetsFilter.
+            properties:
+              controllers:
+                description: |-
+                  Controllers is a list of Gateway API controllers that processed the SnippetsFilter
+                  and the status of the SnippetsFilter with respect to each controller.
+                items:
+                  properties:
+                    conditions:
+                      description: Conditions describe the status of the SnippetsFilter.
+                      items:
+                        description: Condition contains details for one aspect of
+                          the current state of this API Resource.
+                        properties:
+                          lastTransitionTime:
+                            description: |-
+                              lastTransitionTime is the last time the condition transitioned from one status to another.
+                              This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                            format: date-time
+                            type: string
+                          message:
+                            description: |-
+                              message is a human readable message indicating details about the transition.
+                              This may be an empty string.
+                            maxLength: 32768
+                            type: string
+                          observedGeneration:
+                            description: |-
+                              observedGeneration represents the .metadata.generation that the condition was set based upon.
+                              For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                              with respect to the current state of the instance.
+                            format: int64
+                            minimum: 0
+                            type: integer
+                          reason:
+                            description: |-
+                              reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                              Producers of specific condition types may define expected values and meanings for this field,
+                              and whether the values are considered a guaranteed API.
+                              The value should be a CamelCase string.
+                              This field may not be empty.
+                            maxLength: 1024
+                            minLength: 1
+                            pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                            type: string
+                          status:
+                            description: status of the condition, one of True, False,
+                              Unknown.
+                            enum:
+                            - "True"
+                            - "False"
+                            - Unknown
+                            type: string
+                          type:
+                            description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                            maxLength: 316
+                            pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                            type: string
+                        required:
+                        - lastTransitionTime
+                        - message
+                        - reason
+                        - status
+                        - type
+                        type: object
+                      maxItems: 8
+                      minItems: 1
+                      type: array
+                      x-kubernetes-list-map-keys:
+                      - type
+                      x-kubernetes-list-type: map
+                    controllerName:
+                      description: |-
+                        ControllerName is a domain/path string that indicates the name of the
+                        controller that wrote this status. This corresponds with the
+                        controllerName field on GatewayClass.
+
+                        Example: "example.net/gateway-controller".
+
+                        The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
+                        valid Kubernetes names
+                        (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+
+                        Controllers MUST populate this field when writing status. Controllers should ensure that
+                        entries to status populated with their ControllerName are cleaned up when they are no
+                        longer necessary.
+                      maxLength: 253
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+                      type: string
+                  required:
+                  - controllerName
+                  type: object
+                maxItems: 16
+                type: array
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.16.5
+  labels:
+    gateway.networking.k8s.io/policy: direct
+  name: upstreamsettingspolicies.gateway.nginx.org
+spec:
+  group: gateway.nginx.org
+  names:
+    categories:
+    - nginx-gateway-fabric
+    kind: UpstreamSettingsPolicy
+    listKind: UpstreamSettingsPolicyList
+    plural: upstreamsettingspolicies
+    shortNames:
+    - uspolicy
+    singular: upstreamsettingspolicy
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: |-
+          UpstreamSettingsPolicy is a Direct Attached Policy. It provides a way to configure the behavior of
+          the connection between NGINX and the upstream applications.
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: Spec defines the desired state of the UpstreamSettingsPolicy.
+            properties:
+              keepAlive:
+                description: KeepAlive defines the keep-alive settings.
+                properties:
+                  connections:
+                    description: |-
+                      Connections sets the maximum number of idle keep-alive connections to upstream servers that are preserved
+                      in the cache of each nginx worker process. When this number is exceeded, the least recently used
+                      connections are closed.
+                      Directive: https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
+                    format: int32
+                    minimum: 1
+                    type: integer
+                  requests:
+                    description: |-
+                      Requests sets the maximum number of requests that can be served through one keep-alive connection.
+                      After the maximum number of requests are made, the connection is closed.
+                      Directive: https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive_requests
+                    format: int32
+                    minimum: 0
+                    type: integer
+                  time:
+                    description: |-
+                      Time defines the maximum time during which requests can be processed through one keep-alive connection.
+                      After this time is reached, the connection is closed following the subsequent request processing.
+                      Directive: https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive_time
+                    pattern: ^[0-9]{1,4}(ms|s|m|h)?$
+                    type: string
+                  timeout:
+                    description: |-
+                      Timeout defines the keep-alive timeout for upstreams.
+                      Directive: https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive_timeout
+                    pattern: ^[0-9]{1,4}(ms|s|m|h)?$
+                    type: string
+                type: object
+              targetRefs:
+                description: |-
+                  TargetRefs identifies API object(s) to apply the policy to.
+                  Objects must be in the same namespace as the policy.
+                  Support: Service
+                items:
+                  description: |-
+                    LocalPolicyTargetReference identifies an API object to apply a direct or
+                    inherited policy to. This should be used as part of Policy resources
+                    that can target Gateway API resources. For more information on how this
+                    policy attachment model works, and a sample Policy resource, refer to
+                    the policy attachment documentation for Gateway API.
+                  properties:
+                    group:
+                      description: Group is the group of the target resource.
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      description: Kind is kind of the target resource.
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    name:
+                      description: Name is the name of the target resource.
+                      maxLength: 253
+                      minLength: 1
+                      type: string
+                  required:
+                  - group
+                  - kind
+                  - name
+                  type: object
+                maxItems: 16
+                minItems: 1
+                type: array
+                x-kubernetes-validations:
+                - message: 'TargetRefs Kind must be: Service'
+                  rule: self.all(t, t.kind=='Service')
+                - message: TargetRefs Group must be core
+                  rule: self.exists(t, t.group=='') || self.exists(t, t.group=='core')
+              zoneSize:
+                description: |-
+                  ZoneSize is the size of the shared memory zone used by the upstream. This memory zone is used to share
+                  the upstream configuration between nginx worker processes. The more servers that an upstream has,
+                  the larger memory zone is required.
+                  Default: OSS: 512k, Plus: 1m.
+                  Directive: https://nginx.org/en/docs/http/ngx_http_upstream_module.html#zone
+                pattern: ^\d{1,4}(k|m|g)?$
+                type: string
+            required:
+            - targetRefs
+            type: object
+          status:
+            description: Status defines the state of the UpstreamSettingsPolicy.
+            properties:
+              ancestors:
+                description: |-
+                  Ancestors is a list of ancestor resources (usually Gateways) that are
+                  associated with the policy, and the status of the policy with respect to
+                  each ancestor. When this policy attaches to a parent, the controller that
+                  manages the parent and the ancestors MUST add an entry to this list when
+                  the controller first sees the policy and SHOULD update the entry as
+                  appropriate when the relevant ancestor is modified.
+
+                  Note that choosing the relevant ancestor is left to the Policy designers;
+                  an important part of Policy design is designing the right object level at
+                  which to namespace this status.
+
+                  Note also that implementations MUST ONLY populate ancestor status for
+                  the Ancestor resources they are responsible for. Implementations MUST
+                  use the ControllerName field to uniquely identify the entries in this list
+                  that they are responsible for.
+
+                  Note that to achieve this, the list of PolicyAncestorStatus structs
+                  MUST be treated as a map with a composite key, made up of the AncestorRef
+                  and ControllerName fields combined.
+
+                  A maximum of 16 ancestors will be represented in this list. An empty list
+                  means the Policy is not relevant for any ancestors.
+
+                  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.
+                items:
+                  description: |-
+                    PolicyAncestorStatus describes the status of a route with respect to an
+                    associated Ancestor.
+
+                    Ancestors refer to objects that are either the Target of a policy or above it
+                    in terms of object hierarchy. For example, if a policy targets a Service, the
+                    Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and
+                    the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most
+                    useful object to place Policy status on, so we recommend that implementations
+                    SHOULD use Gateway as the PolicyAncestorStatus object unless the designers
+                    have a _very_ good reason otherwise.
+
+                    In the context of policy attachment, the Ancestor is used to distinguish which
+                    resource results in a distinct application of this policy. For example, if a policy
+                    targets a Service, it may have a distinct result per attached Gateway.
+
+                    Policies targeting the same resource may have different effects depending on the
+                    ancestors of those resources. For example, different Gateways targeting the same
+                    Service may have different capabilities, especially if they have different underlying
+                    implementations.
+
+                    For example, in BackendTLSPolicy, the Policy attaches to a Service that is
+                    used as a backend in a HTTPRoute that is itself attached to a Gateway.
+                    In this case, the relevant object for status is the Gateway, and that is the
+                    ancestor object referred to in this status.
+
+                    Note that a parent is also an ancestor, so for objects where the parent is the
+                    relevant object for status, this struct SHOULD still be used.
+
+                    This struct is intended to be used in a slice that's effectively a map,
+                    with a composite key made up of the AncestorRef and the ControllerName.
+                  properties:
+                    ancestorRef:
+                      description: |-
+                        AncestorRef corresponds with a ParentRef in the spec that this
+                        PolicyAncestorStatus struct describes the status of.
+                      properties:
+                        group:
+                          default: gateway.networking.k8s.io
+                          description: |-
+                            Group is the group of the referent.
+                            When unspecified, "gateway.networking.k8s.io" is inferred.
+                            To set the core API group (such as for a "Service" kind referent),
+                            Group must be explicitly set to "" (empty string).
+
+                            Support: Core
+                          maxLength: 253
+                          pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                          type: string
+                        kind:
+                          default: Gateway
+                          description: |-
+                            Kind is kind of the referent.
+
+                            There are two kinds of parent resources with "Core" support:
+
+                            * Gateway (Gateway conformance profile)
+                            * Service (Mesh conformance profile, ClusterIP Services only)
+
+                            Support for other resources is Implementation-Specific.
+                          maxLength: 63
+                          minLength: 1
+                          pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                          type: string
+                        name:
+                          description: |-
+                            Name is the name of the referent.
+
+                            Support: Core
+                          maxLength: 253
+                          minLength: 1
+                          type: string
+                        namespace:
+                          description: |-
+                            Namespace is the namespace of the referent. When unspecified, this refers
+                            to the local namespace of the Route.
+
+                            Note that there are specific rules for ParentRefs which cross namespace
+                            boundaries. Cross-namespace references are only valid if they are explicitly
+                            allowed by something in the namespace they are referring to. For example:
+                            Gateway has the AllowedRoutes field, and ReferenceGrant provides a
+                            generic way to enable any other kind of cross-namespace reference.
+
+                            
+                            ParentRefs from a Route to a Service in the same namespace are "producer"
+                            routes, which apply default routing rules to inbound connections from
+                            any namespace to the Service.
+
+                            ParentRefs from a Route to a Service in a different namespace are
+                            "consumer" routes, and these routing rules are only applied to outbound
+                            connections originating from the same namespace as the Route, for which
+                            the intended destination of the connections are a Service targeted as a
+                            ParentRef of the Route.
+                            
+
+                            Support: Core
+                          maxLength: 63
+                          minLength: 1
+                          pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                          type: string
+                        port:
+                          description: |-
+                            Port is the network port this Route targets. It can be interpreted
+                            differently based on the type of parent resource.
+
+                            When the parent resource is a Gateway, this targets all listeners
+                            listening on the specified port that also support this kind of Route(and
+                            select this Route). It's not recommended to set `Port` unless the
+                            networking behaviors specified in a Route must apply to a specific port
+                            as opposed to a listener(s) whose port(s) may be changed. When both Port
+                            and SectionName are specified, the name and port of the selected listener
+                            must match both specified values.
+
+                            
+                            When the parent resource is a Service, this targets a specific port in the
+                            Service spec. When both Port (experimental) and SectionName are specified,
+                            the name and port of the selected port must match both specified values.
+                            
+
+                            Implementations MAY choose to support other parent resources.
+                            Implementations supporting other types of parent resources MUST clearly
+                            document how/if Port is interpreted.
+
+                            For the purpose of status, an attachment is considered successful as
+                            long as the parent resource accepts it partially. For example, Gateway
+                            listeners can restrict which Routes can attach to them by Route kind,
+                            namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
+                            from the referencing Route, the Route MUST be considered successfully
+                            attached. If no Gateway listeners accept attachment from this Route,
+                            the Route MUST be considered detached from the Gateway.
+
+                            Support: Extended
+                          format: int32
+                          maximum: 65535
+                          minimum: 1
+                          type: integer
+                        sectionName:
+                          description: |-
+                            SectionName is the name of a section within the target resource. In the
+                            following resources, SectionName is interpreted as the following:
+
+                            * Gateway: Listener name. When both Port (experimental) and SectionName
+                            are specified, the name and port of the selected listener must match
+                            both specified values.
+                            * Service: Port name. When both Port (experimental) and SectionName
+                            are specified, the name and port of the selected listener must match
+                            both specified values.
+
+                            Implementations MAY choose to support attaching Routes to other resources.
+                            If that is the case, they MUST clearly document how SectionName is
+                            interpreted.
+
+                            When unspecified (empty string), this will reference the entire resource.
+                            For the purpose of status, an attachment is considered successful if at
+                            least one section in the parent resource accepts it. For example, Gateway
+                            listeners can restrict which Routes can attach to them by Route kind,
+                            namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
+                            the referencing Route, the Route MUST be considered successfully
+                            attached. If no Gateway listeners accept attachment from this Route, the
+                            Route MUST be considered detached from the Gateway.
+
+                            Support: Core
+                          maxLength: 253
+                          minLength: 1
+                          pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                          type: string
+                      required:
+                      - name
+                      type: object
+                    conditions:
+                      description: Conditions describes the status of the Policy with
+                        respect to the given Ancestor.
+                      items:
+                        description: Condition contains details for one aspect of
+                          the current state of this API Resource.
+                        properties:
+                          lastTransitionTime:
+                            description: |-
+                              lastTransitionTime is the last time the condition transitioned from one status to another.
+                              This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                            format: date-time
+                            type: string
+                          message:
+                            description: |-
+                              message is a human readable message indicating details about the transition.
+                              This may be an empty string.
+                            maxLength: 32768
+                            type: string
+                          observedGeneration:
+                            description: |-
+                              observedGeneration represents the .metadata.generation that the condition was set based upon.
+                              For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                              with respect to the current state of the instance.
+                            format: int64
+                            minimum: 0
+                            type: integer
+                          reason:
+                            description: |-
+                              reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                              Producers of specific condition types may define expected values and meanings for this field,
+                              and whether the values are considered a guaranteed API.
+                              The value should be a CamelCase string.
+                              This field may not be empty.
+                            maxLength: 1024
+                            minLength: 1
+                            pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                            type: string
+                          status:
+                            description: status of the condition, one of True, False,
+                              Unknown.
+                            enum:
+                            - "True"
+                            - "False"
+                            - Unknown
+                            type: string
+                          type:
+                            description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                            maxLength: 316
+                            pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                            type: string
+                        required:
+                        - lastTransitionTime
+                        - message
+                        - reason
+                        - status
+                        - type
+                        type: object
+                      maxItems: 8
+                      minItems: 1
+                      type: array
+                      x-kubernetes-list-map-keys:
+                      - type
+                      x-kubernetes-list-type: map
+                    controllerName:
+                      description: |-
+                        ControllerName is a domain/path string that indicates the name of the
+                        controller that wrote this status. This corresponds with the
+                        controllerName field on GatewayClass.
+
+                        Example: "example.net/gateway-controller".
+
+                        The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
+                        valid Kubernetes names
+                        (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+
+                        Controllers MUST populate this field when writing status. Controllers should ensure that
+                        entries to status populated with their ControllerName are cleaned up when they are no
+                        longer necessary.
+                      maxLength: 253
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+                      type: string
+                  required:
+                  - ancestorRef
+                  - controllerName
+                  type: object
+                maxItems: 16
+                type: array
+            required:
+            - ancestors
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/deploy/default/deploy.yaml b/deploy/default/deploy.yaml
index 88aae1eedd..5eab1d6403 100644
--- a/deploy/default/deploy.yaml
+++ b/deploy/default/deploy.yaml
@@ -98,6 +98,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   verbs:
   - list
   - watch
@@ -107,6 +108,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   verbs:
   - update
 - apiGroups:
diff --git a/deploy/experimental-nginx-plus/deploy.yaml b/deploy/experimental-nginx-plus/deploy.yaml
index c2bcbafe09..8f8e1cc57a 100644
--- a/deploy/experimental-nginx-plus/deploy.yaml
+++ b/deploy/experimental-nginx-plus/deploy.yaml
@@ -111,6 +111,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   verbs:
   - list
   - watch
@@ -120,6 +121,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   verbs:
   - update
 - apiGroups:
diff --git a/deploy/experimental/deploy.yaml b/deploy/experimental/deploy.yaml
index be62207472..d22fff1298 100644
--- a/deploy/experimental/deploy.yaml
+++ b/deploy/experimental/deploy.yaml
@@ -103,6 +103,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   verbs:
   - list
   - watch
@@ -112,6 +113,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   verbs:
   - update
 - apiGroups:
diff --git a/deploy/nginx-plus/deploy.yaml b/deploy/nginx-plus/deploy.yaml
index f31b0da07b..fed7a22d11 100644
--- a/deploy/nginx-plus/deploy.yaml
+++ b/deploy/nginx-plus/deploy.yaml
@@ -106,6 +106,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   verbs:
   - list
   - watch
@@ -115,6 +116,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   verbs:
   - update
 - apiGroups:
diff --git a/deploy/nodeport/deploy.yaml b/deploy/nodeport/deploy.yaml
index 25b6210ed1..772304fb1f 100644
--- a/deploy/nodeport/deploy.yaml
+++ b/deploy/nodeport/deploy.yaml
@@ -98,6 +98,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   verbs:
   - list
   - watch
@@ -107,6 +108,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   verbs:
   - update
 - apiGroups:
diff --git a/deploy/openshift/deploy.yaml b/deploy/openshift/deploy.yaml
index 8231de661e..fc422eab2e 100644
--- a/deploy/openshift/deploy.yaml
+++ b/deploy/openshift/deploy.yaml
@@ -98,6 +98,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   verbs:
   - list
   - watch
@@ -107,6 +108,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   verbs:
   - update
 - apiGroups:
diff --git a/deploy/snippets-filters-nginx-plus/deploy.yaml b/deploy/snippets-filters-nginx-plus/deploy.yaml
index 4a68115cce..c609ea4810 100644
--- a/deploy/snippets-filters-nginx-plus/deploy.yaml
+++ b/deploy/snippets-filters-nginx-plus/deploy.yaml
@@ -106,6 +106,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   - snippetsfilters
   verbs:
   - list
@@ -116,6 +117,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   - snippetsfilters/status
   verbs:
   - update
diff --git a/deploy/snippets-filters/deploy.yaml b/deploy/snippets-filters/deploy.yaml
index e6fd79ce24..02d310f524 100644
--- a/deploy/snippets-filters/deploy.yaml
+++ b/deploy/snippets-filters/deploy.yaml
@@ -98,6 +98,7 @@ rules:
   - nginxproxies
   - clientsettingspolicies
   - observabilitypolicies
+  - upstreamsettingspolicies
   - snippetsfilters
   verbs:
   - list
@@ -108,6 +109,7 @@ rules:
   - nginxgateways/status
   - clientsettingspolicies/status
   - observabilitypolicies/status
+  - upstreamsettingspolicies/status
   - snippetsfilters/status
   verbs:
   - update
diff --git a/examples/upstream-settings-policy/cafe-routes.yaml b/examples/upstream-settings-policy/cafe-routes.yaml
new file mode 100644
index 0000000000..67927335cb
--- /dev/null
+++ b/examples/upstream-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/upstream-settings-policy/cafe.yaml b/examples/upstream-settings-policy/cafe.yaml
new file mode 100644
index 0000000000..2d03ae59ff
--- /dev/null
+++ b/examples/upstream-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/upstream-settings-policy/gateway.yaml b/examples/upstream-settings-policy/gateway.yaml
new file mode 100644
index 0000000000..e6507f613b
--- /dev/null
+++ b/examples/upstream-settings-policy/gateway.yaml
@@ -0,0 +1,11 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway
+spec:
+  gatewayClassName: nginx
+  listeners:
+  - name: http
+    port: 80
+    protocol: HTTP
+    hostname: "*.example.com"
diff --git a/examples/upstream-settings-policy/upstream-settings-policy.yaml b/examples/upstream-settings-policy/upstream-settings-policy.yaml
index e49ed1dffd..95e8a34e6d 100644
--- a/examples/upstream-settings-policy/upstream-settings-policy.yaml
+++ b/examples/upstream-settings-policy/upstream-settings-policy.yaml
@@ -7,7 +7,7 @@ spec:
   targetRefs:
     - group: core
       kind: Service
-      name: service
+      name: coffee
   keepAlive:
     connections: 32
     requests: 1001
diff --git a/internal/framework/kinds/kinds.go b/internal/framework/kinds/kinds.go
index 7700ad39ce..baeda6f9ee 100644
--- a/internal/framework/kinds/kinds.go
+++ b/internal/framework/kinds/kinds.go
@@ -11,9 +11,9 @@ import (
 
 // Gateway API Kinds.
 const (
-	// Gateway is the Gateway Kind.
+	// Gateway is the Gateway kind.
 	Gateway = "Gateway"
-	// GatewayClass is the GatewayClass Kind.
+	// GatewayClass is the GatewayClass kind.
 	GatewayClass = "GatewayClass"
 	// HTTPRoute is the HTTPRoute kind.
 	HTTPRoute = "HTTPRoute"
@@ -23,6 +23,12 @@ const (
 	TLSRoute = "TLSRoute"
 )
 
+// Core API Kinds.
+const (
+	// Service is the Service kind.
+	Service = "Service"
+)
+
 // NGINX Gateway Fabric kinds.
 const (
 	// ClientSettingsPolicy is the ClientSettingsPolicy kind.
@@ -33,6 +39,8 @@ const (
 	NginxProxy = "NginxProxy"
 	// SnippetsFilter is the SnippetsFilter kind.
 	SnippetsFilter = "SnippetsFilter"
+	// UpstreamSettingsPolicy is the UpstreamSettingsPolicy kind.
+	UpstreamSettingsPolicy = "UpstreamSettingsPolicy"
 )
 
 // MustExtractGVK is a function that extracts the GroupVersionKind (GVK) of a client.object.
diff --git a/internal/mode/static/manager.go b/internal/mode/static/manager.go
index 4c80fc4260..80738018d2 100644
--- a/internal/mode/static/manager.go
+++ b/internal/mode/static/manager.go
@@ -52,6 +52,7 @@ import (
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies/clientsettings"
 	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies/observability"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies/upstreamsettings"
 	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"
@@ -311,6 +312,10 @@ func createPolicyManager(
 			GVK:       mustExtractGVK(&ngfAPI.ObservabilityPolicy{}),
 			Validator: observability.NewValidator(validator),
 		},
+		{
+			GVK:       mustExtractGVK(&ngfAPI.UpstreamSettingsPolicy{}),
+			Validator: upstreamsettings.NewValidator(validator),
+		},
 	}
 
 	return policies.NewManager(mustExtractGVK, cfgs...)
@@ -492,6 +497,12 @@ func registerControllers(
 				controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
 			},
 		},
+		{
+			objectType: &ngfAPI.UpstreamSettingsPolicy{},
+			options: []controller.Option{
+				controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
+			},
+		},
 	}
 
 	if cfg.ExperimentalFeatures {
@@ -728,6 +739,7 @@ func prepareFirstEventBatchPreparerArgs(cfg config.Config) ([]client.Object, []c
 		&gatewayv1.GRPCRouteList{},
 		&ngfAPI.ClientSettingsPolicyList{},
 		&ngfAPI.ObservabilityPolicyList{},
+		&ngfAPI.UpstreamSettingsPolicyList{},
 		partialObjectMetadataList,
 	}
 
diff --git a/internal/mode/static/manager_test.go b/internal/mode/static/manager_test.go
index 5a61a75854..0f2d802d32 100644
--- a/internal/mode/static/manager_test.go
+++ b/internal/mode/static/manager_test.go
@@ -67,6 +67,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
 				partialObjectMetadataList,
 				&ngfAPI.ClientSettingsPolicyList{},
 				&ngfAPI.ObservabilityPolicyList{},
+				&ngfAPI.UpstreamSettingsPolicyList{},
 			},
 		},
 		{
@@ -96,6 +97,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
 				partialObjectMetadataList,
 				&ngfAPI.ClientSettingsPolicyList{},
 				&ngfAPI.ObservabilityPolicyList{},
+				&ngfAPI.UpstreamSettingsPolicyList{},
 			},
 		},
 		{
@@ -128,6 +130,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
 				&gatewayv1.GRPCRouteList{},
 				&ngfAPI.ClientSettingsPolicyList{},
 				&ngfAPI.ObservabilityPolicyList{},
+				&ngfAPI.UpstreamSettingsPolicyList{},
 			},
 		},
 		{
@@ -158,6 +161,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
 				&ngfAPI.ClientSettingsPolicyList{},
 				&ngfAPI.ObservabilityPolicyList{},
 				&ngfAPI.SnippetsFilterList{},
+				&ngfAPI.UpstreamSettingsPolicyList{},
 			},
 		},
 		{
@@ -191,6 +195,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
 				&ngfAPI.ClientSettingsPolicyList{},
 				&ngfAPI.ObservabilityPolicyList{},
 				&ngfAPI.SnippetsFilterList{},
+				&ngfAPI.UpstreamSettingsPolicyList{},
 			},
 		},
 	}
diff --git a/internal/mode/static/nginx/config/policies/clientsettings/validator.go b/internal/mode/static/nginx/config/policies/clientsettings/validator.go
index 60ce55c7a9..79a390ce9b 100644
--- a/internal/mode/static/nginx/config/policies/clientsettings/validator.go
+++ b/internal/mode/static/nginx/config/policies/clientsettings/validator.go
@@ -30,7 +30,9 @@ func (v *Validator) Validate(policy policies.Policy, _ *policies.GlobalSettings)
 
 	targetRefPath := field.NewPath("spec").Child("targetRef")
 	supportedKinds := []gatewayv1.Kind{kinds.Gateway, kinds.HTTPRoute, kinds.GRPCRoute}
-	if err := policies.ValidateTargetRef(csp.Spec.TargetRef, targetRefPath, supportedKinds); err != nil {
+	supportedGroups := []gatewayv1.Group{gatewayv1.GroupName}
+
+	if err := policies.ValidateTargetRef(csp.Spec.TargetRef, targetRefPath, supportedGroups, supportedKinds); err != nil {
 		return []conditions.Condition{staticConds.NewPolicyInvalid(err.Error())}
 	}
 
diff --git a/internal/mode/static/nginx/config/policies/observability/validator.go b/internal/mode/static/nginx/config/policies/observability/validator.go
index 3fa7ea2289..b93902d958 100644
--- a/internal/mode/static/nginx/config/policies/observability/validator.go
+++ b/internal/mode/static/nginx/config/policies/observability/validator.go
@@ -45,8 +45,10 @@ func (v *Validator) Validate(
 
 	targetRefPath := field.NewPath("spec").Child("targetRefs")
 	supportedKinds := []gatewayv1.Kind{kinds.HTTPRoute, kinds.GRPCRoute}
+	supportedGroups := []gatewayv1.Group{gatewayv1.GroupName}
+
 	for _, ref := range obs.Spec.TargetRefs {
-		if err := policies.ValidateTargetRef(ref, targetRefPath, supportedKinds); err != nil {
+		if err := policies.ValidateTargetRef(ref, targetRefPath, supportedGroups, supportedKinds); err != nil {
 			return []conditions.Condition{staticConds.NewPolicyInvalid(err.Error())}
 		}
 	}
diff --git a/internal/mode/static/nginx/config/policies/policy.go b/internal/mode/static/nginx/config/policies/policy.go
index c26f3bec7b..b818f5a216 100644
--- a/internal/mode/static/nginx/config/policies/policy.go
+++ b/internal/mode/static/nginx/config/policies/policy.go
@@ -24,9 +24,9 @@ type Policy interface {
 // GlobalSettings contains global settings from the current state of the graph that may be
 // needed for policy validation or generation if certain policies rely on those global settings.
 type GlobalSettings struct {
-	// NginxProxyValid is whether or not the NginxProxy resource is valid.
+	// NginxProxyValid is whether the NginxProxy resource is valid.
 	NginxProxyValid bool
-	// TelemetryEnabled is whether or not telemetry is enabled in the NginxProxy resource.
+	// TelemetryEnabled is whether telemetry is enabled in the NginxProxy resource.
 	TelemetryEnabled bool
 }
 
@@ -34,15 +34,16 @@ type GlobalSettings struct {
 func ValidateTargetRef(
 	ref v1alpha2.LocalPolicyTargetReference,
 	basePath *field.Path,
+	groups []gatewayv1.Group,
 	supportedKinds []gatewayv1.Kind,
 ) error {
-	if ref.Group != gatewayv1.GroupName {
+	if !slices.Contains(groups, ref.Group) {
 		path := basePath.Child("group")
 
 		return field.NotSupported(
 			path,
 			ref.Group,
-			[]string{gatewayv1.GroupName},
+			groups,
 		)
 	}
 
diff --git a/internal/mode/static/nginx/config/policies/upstreamsettings/validator.go b/internal/mode/static/nginx/config/policies/upstreamsettings/validator.go
new file mode 100644
index 0000000000..9fccebd166
--- /dev/null
+++ b/internal/mode/static/nginx/config/policies/upstreamsettings/validator.go
@@ -0,0 +1,125 @@
+package upstreamsettings
+
+import (
+	"k8s.io/apimachinery/pkg/util/validation/field"
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+	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"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies"
+	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation"
+)
+
+// Validator validates an UpstreamSettingsPolicy.
+// Implements policies.Validator interface.
+type Validator struct {
+	genericValidator validation.GenericValidator
+}
+
+// NewValidator returns a new Validator.
+func NewValidator(genericValidator validation.GenericValidator) Validator {
+	return Validator{genericValidator: genericValidator}
+}
+
+// Validate validates the spec of an UpstreamsSettingsPolicy.
+func (v Validator) Validate(policy policies.Policy, _ *policies.GlobalSettings) []conditions.Condition {
+	usp := helpers.MustCastObject[*ngfAPI.UpstreamSettingsPolicy](policy)
+
+	targetRefsPath := field.NewPath("spec").Child("targetRefs")
+	supportedKinds := []gatewayv1.Kind{kinds.Service}
+	supportedGroups := []gatewayv1.Group{"", "core"}
+
+	for i, ref := range usp.Spec.TargetRefs {
+		indexedPath := targetRefsPath.Index(i)
+		if err := policies.ValidateTargetRef(ref, indexedPath, supportedGroups, supportedKinds); err != nil {
+			return []conditions.Condition{staticConds.NewPolicyInvalid(err.Error())}
+		}
+	}
+
+	if err := v.validateSettings(usp.Spec); err != nil {
+		return []conditions.Condition{staticConds.NewPolicyInvalid(err.Error())}
+	}
+
+	return nil
+}
+
+// Conflicts returns true if the two UpstreamsSettingsPolicies conflict.
+func (v Validator) Conflicts(polA, polB policies.Policy) bool {
+	cspA := helpers.MustCastObject[*ngfAPI.UpstreamSettingsPolicy](polA)
+	cspB := helpers.MustCastObject[*ngfAPI.UpstreamSettingsPolicy](polB)
+
+	return conflicts(cspA.Spec, cspB.Spec)
+}
+
+func conflicts(a, b ngfAPI.UpstreamSettingsPolicySpec) bool {
+	if a.ZoneSize != nil && b.ZoneSize != nil {
+		return true
+	}
+
+	if a.KeepAlive != nil && b.KeepAlive != nil {
+		if a.KeepAlive.Connections != nil && b.KeepAlive.Connections != nil {
+			return true
+		}
+		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
+}
+
+// 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.UpstreamSettingsPolicySpec) error {
+	var allErrs field.ErrorList
+	fieldPath := field.NewPath("spec")
+
+	if spec.ZoneSize != nil {
+		if err := v.genericValidator.ValidateNginxSize(string(*spec.ZoneSize)); err != nil {
+			path := fieldPath.Child("zoneSize")
+			allErrs = append(allErrs, field.Invalid(path, spec.ZoneSize, err.Error()))
+		}
+	}
+
+	if spec.KeepAlive != nil {
+		allErrs = append(allErrs, v.validateUpstreamKeepAlive(*spec.KeepAlive, fieldPath.Child("keepAlive"))...)
+	}
+
+	return allErrs.ToAggregate()
+}
+
+func (v Validator) validateUpstreamKeepAlive(
+	keepAlive ngfAPI.UpstreamKeepAlive,
+	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 {
+		if err := v.genericValidator.ValidateNginxDuration(string(*keepAlive.Timeout)); err != nil {
+			path := fieldPath.Child("timeout")
+
+			allErrs = append(allErrs, field.Invalid(path, *keepAlive.Timeout, err.Error()))
+		}
+	}
+
+	return allErrs
+}
diff --git a/internal/mode/static/nginx/config/policies/upstreamsettings/validator_test.go b/internal/mode/static/nginx/config/policies/upstreamsettings/validator_test.go
new file mode 100644
index 0000000000..3e303a8229
--- /dev/null
+++ b/internal/mode/static/nginx/config/policies/upstreamsettings/validator_test.go
@@ -0,0 +1,266 @@
+package upstreamsettings_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/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/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/nginx/config/policies/policiesfakes"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies/upstreamsettings"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/validation"
+	staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
+)
+
+type policyModFunc func(policy *ngfAPI.UpstreamSettingsPolicy) *ngfAPI.UpstreamSettingsPolicy
+
+func createValidPolicy() *ngfAPI.UpstreamSettingsPolicy {
+	return &ngfAPI.UpstreamSettingsPolicy{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+		},
+		Spec: ngfAPI.UpstreamSettingsPolicySpec{
+			TargetRefs: []v1alpha2.LocalPolicyTargetReference{
+				{
+					Group: "core",
+					Kind:  kinds.Service,
+					Name:  "svc",
+				},
+			},
+			ZoneSize: helpers.GetPointer[ngfAPI.Size]("1k"),
+			KeepAlive: &ngfAPI.UpstreamKeepAlive{
+				Requests:    helpers.GetPointer[int32](900),
+				Time:        helpers.GetPointer[ngfAPI.Duration]("50s"),
+				Timeout:     helpers.GetPointer[ngfAPI.Duration]("30s"),
+				Connections: helpers.GetPointer[int32](100),
+			},
+		},
+		Status: v1alpha2.PolicyStatus{},
+	}
+}
+
+func createModifiedPolicy(mod policyModFunc) *ngfAPI.UpstreamSettingsPolicy {
+	return mod(createValidPolicy())
+}
+
+func TestValidator_Validate(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name          string
+		policy        *ngfAPI.UpstreamSettingsPolicy
+		expConditions []conditions.Condition
+	}{
+		{
+			name: "invalid target ref; unsupported group",
+			policy: createModifiedPolicy(func(p *ngfAPI.UpstreamSettingsPolicy) *ngfAPI.UpstreamSettingsPolicy {
+				p.Spec.TargetRefs = append(
+					p.Spec.TargetRefs,
+					v1alpha2.LocalPolicyTargetReference{
+						Group: "Unsupported",
+						Kind:  kinds.Service,
+						Name:  "svc",
+					})
+				return p
+			}),
+			expConditions: []conditions.Condition{
+				staticConds.NewPolicyInvalid("spec.targetRefs[1].group: Unsupported value: \"Unsupported\": " +
+					"supported values: \"\", \"core\""),
+			},
+		},
+		{
+			name: "invalid target ref; unsupported kind",
+			policy: createModifiedPolicy(func(p *ngfAPI.UpstreamSettingsPolicy) *ngfAPI.UpstreamSettingsPolicy {
+				p.Spec.TargetRefs = append(
+					p.Spec.TargetRefs,
+					v1alpha2.LocalPolicyTargetReference{
+						Group: "",
+						Kind:  "Unsupported",
+						Name:  "svc",
+					})
+				return p
+			}),
+			expConditions: []conditions.Condition{
+				staticConds.NewPolicyInvalid("spec.targetRefs[1].kind: Unsupported value: \"Unsupported\": " +
+					"supported values: \"Service\""),
+			},
+		},
+		{
+			name: "invalid zone size",
+			policy: createModifiedPolicy(func(p *ngfAPI.UpstreamSettingsPolicy) *ngfAPI.UpstreamSettingsPolicy {
+				p.Spec.ZoneSize = helpers.GetPointer[ngfAPI.Size]("invalid")
+				return p
+			}),
+			expConditions: []conditions.Condition{
+				staticConds.NewPolicyInvalid("spec.zoneSize: Invalid value: \"invalid\": ^\\d{1,4}(k|m|g)?$ " +
+					"(e.g. '1024',  or '8k',  or '20m',  or '1g', regex used for validation is 'must contain a number. " +
+					"May be followed by 'k', 'm', or 'g', otherwise bytes are assumed')"),
+			},
+		},
+		{
+			name: "invalid durations",
+			policy: createModifiedPolicy(func(p *ngfAPI.UpstreamSettingsPolicy) *ngfAPI.UpstreamSettingsPolicy {
+				p.Spec.KeepAlive.Time = helpers.GetPointer[ngfAPI.Duration]("invalid")
+				p.Spec.KeepAlive.Timeout = helpers.GetPointer[ngfAPI.Duration]("invalid")
+				return p
+			}),
+			expConditions: []conditions.Condition{
+				staticConds.NewPolicyInvalid(
+					"[spec.keepAlive.time: Invalid value: \"invalid\": ^[0-9]{1,4}(ms|s|m|h)? " +
+						"(e.g. '5ms',  or '10s',  or '500m',  or '1000h', regex used for validation is " +
+						"'must contain an, at most, four digit number followed by 'ms', 's', 'm', or 'h''), " +
+						"spec.keepAlive.timeout: Invalid value: \"invalid\": ^[0-9]{1,4}(ms|s|m|h)? " +
+						"(e.g. '5ms',  or '10s',  or '500m',  or '1000h', regex used for validation is " +
+						"'must contain an, at most, four digit number followed by 'ms', 's', 'm', or 'h'')]"),
+			},
+		},
+		{
+			name:          "valid",
+			policy:        createValidPolicy(),
+			expConditions: nil,
+		},
+	}
+
+	v := upstreamsettings.NewValidator(validation.GenericValidator{})
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			t.Parallel()
+			g := NewWithT(t)
+
+			conds := v.Validate(test.policy, nil)
+			g.Expect(conds).To(Equal(test.expConditions))
+		})
+	}
+}
+
+func TestValidator_ValidatePanics(t *testing.T) {
+	t.Parallel()
+	v := upstreamsettings.NewValidator(nil)
+
+	validate := func() {
+		_ = v.Validate(&policiesfakes.FakePolicy{}, nil)
+	}
+
+	g := NewWithT(t)
+
+	g.Expect(validate).To(Panic())
+}
+
+func TestValidator_Conflicts(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		polA      *ngfAPI.UpstreamSettingsPolicy
+		polB      *ngfAPI.UpstreamSettingsPolicy
+		name      string
+		conflicts bool
+	}{
+		{
+			name: "no conflicts",
+			polA: &ngfAPI.UpstreamSettingsPolicy{
+				Spec: ngfAPI.UpstreamSettingsPolicySpec{
+					ZoneSize: helpers.GetPointer[ngfAPI.Size]("10m"),
+					KeepAlive: &ngfAPI.UpstreamKeepAlive{
+						Requests: helpers.GetPointer[int32](900),
+						Time:     helpers.GetPointer[ngfAPI.Duration]("50s"),
+					},
+				},
+			},
+			polB: &ngfAPI.UpstreamSettingsPolicy{
+				Spec: ngfAPI.UpstreamSettingsPolicySpec{
+					KeepAlive: &ngfAPI.UpstreamKeepAlive{
+						Timeout:     helpers.GetPointer[ngfAPI.Duration]("30s"),
+						Connections: helpers.GetPointer[int32](50),
+					},
+				},
+			},
+			conflicts: false,
+		},
+		{
+			name: "zone max size conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.UpstreamSettingsPolicy{
+				Spec: ngfAPI.UpstreamSettingsPolicySpec{
+					ZoneSize: helpers.GetPointer[ngfAPI.Size]("10m"),
+				},
+			},
+			conflicts: true,
+		},
+		{
+			name: "keepalive requests conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.UpstreamSettingsPolicy{
+				Spec: ngfAPI.UpstreamSettingsPolicySpec{
+					KeepAlive: &ngfAPI.UpstreamKeepAlive{
+						Requests: helpers.GetPointer[int32](900),
+					},
+				},
+			},
+			conflicts: true,
+		},
+		{
+			name: "keepalive connections conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.UpstreamSettingsPolicy{
+				Spec: ngfAPI.UpstreamSettingsPolicySpec{
+					KeepAlive: &ngfAPI.UpstreamKeepAlive{
+						Connections: helpers.GetPointer[int32](900),
+					},
+				},
+			},
+			conflicts: true,
+		},
+		{
+			name: "keepalive time conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.UpstreamSettingsPolicy{
+				Spec: ngfAPI.UpstreamSettingsPolicySpec{
+					KeepAlive: &ngfAPI.UpstreamKeepAlive{
+						Time: helpers.GetPointer[ngfAPI.Duration]("50s"),
+					},
+				},
+			},
+			conflicts: true,
+		},
+		{
+			name: "keepalive timeout conflicts",
+			polA: createValidPolicy(),
+			polB: &ngfAPI.UpstreamSettingsPolicy{
+				Spec: ngfAPI.UpstreamSettingsPolicySpec{
+					KeepAlive: &ngfAPI.UpstreamKeepAlive{
+						Timeout: helpers.GetPointer[ngfAPI.Duration]("30s"),
+					},
+				},
+			},
+			conflicts: true,
+		},
+	}
+
+	v := upstreamsettings.NewValidator(nil)
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			t.Parallel()
+			g := NewWithT(t)
+
+			g.Expect(v.Conflicts(test.polA, test.polB)).To(Equal(test.conflicts))
+		})
+	}
+}
+
+func TestValidator_ConflictsPanics(t *testing.T) {
+	t.Parallel()
+	v := upstreamsettings.NewValidator(nil)
+
+	conflicts := func() {
+		_ = v.Conflicts(&policiesfakes.FakePolicy{}, &policiesfakes.FakePolicy{})
+	}
+
+	g := NewWithT(t)
+
+	g.Expect(conflicts).To(Panic())
+}
diff --git a/internal/mode/static/nginx/config/upstreams_template.go b/internal/mode/static/nginx/config/upstreams_template.go
index 47fac2dd00..88a588d136 100644
--- a/internal/mode/static/nginx/config/upstreams_template.go
+++ b/internal/mode/static/nginx/config/upstreams_template.go
@@ -14,7 +14,7 @@ upstream {{ $u.Name }} {
     {{ if $u.ZoneSize -}}
     zone {{ $u.Name }} {{ $u.ZoneSize }};
     {{- end }}
-    {{ range $server := $u.Servers }}
+    {{ range $server := $u.Servers -}}
     server {{ $server.Address }};
     {{- end }}
     {{ if $u.KeepAlive.Connections -}}
diff --git a/internal/mode/static/state/change_processor.go b/internal/mode/static/state/change_processor.go
index cb35491209..82edbcc7b1 100644
--- a/internal/mode/static/state/change_processor.go
+++ b/internal/mode/static/state/change_processor.go
@@ -216,6 +216,11 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl {
 				store:     commonPolicyObjectStore,
 				predicate: funcPredicate{stateChanged: isNGFPolicyRelevant},
 			},
+			{
+				gvk:       cfg.MustExtractGVK(&ngfAPI.UpstreamSettingsPolicy{}),
+				store:     commonPolicyObjectStore,
+				predicate: funcPredicate{stateChanged: isNGFPolicyRelevant},
+			},
 			{
 				gvk:       cfg.MustExtractGVK(&v1alpha2.TLSRoute{}),
 				store:     newObjectStoreMapAdapter(clusterStore.TLSRoutes),
diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go
index d1ec0d3c2c..1f51688506 100644
--- a/internal/mode/static/state/change_processor_test.go
+++ b/internal/mode/static/state/change_processor_test.go
@@ -213,13 +213,11 @@ func createHTTPBackendRef(
 	}
 }
 
-func createTLSBackendRef(
-	name v1.ObjectName,
-	namespace v1.Namespace,
-) v1.BackendRef {
+func createTLSBackendRef(name, namespace string) v1.BackendRef {
 	kindSvc := v1.Kind("Service")
+	ns := v1.Namespace(namespace)
 	return v1.BackendRef{
-		BackendObjectReference: createBackendRefObj(&kindSvc, name, &namespace),
+		BackendObjectReference: createBackendRefObj(&kindSvc, v1.ObjectName(name), &ns),
 	}
 }
 
@@ -371,17 +369,21 @@ var _ = Describe("ChangeProcessor", func() {
 				gatewayAPICRD, gatewayAPICRDUpdated                  *metav1.PartialObjectMetadata
 				routeKey1, routeKey2                                 graph.RouteKey
 				trKey1, trKey2                                       graph.L4RouteKey
+				refSvc, refTLSSvc                                    types.NamespacedName
 			)
 			BeforeAll(func() {
 				gcUpdated = gc.DeepCopy()
 				gcUpdated.Generation++
 
+				refSvc = types.NamespacedName{Namespace: "service-ns", Name: "service"}
+				refTLSSvc = types.NamespacedName{Namespace: "tls-service-ns", Name: "tls-service"}
+
 				crossNsBackendRef := v1.HTTPBackendRef{
 					BackendRef: v1.BackendRef{
 						BackendObjectReference: v1.BackendObjectReference{
 							Kind:      helpers.GetPointer[v1.Kind]("Service"),
-							Name:      "service",
-							Namespace: helpers.GetPointer[v1.Namespace]("service-ns"),
+							Name:      v1.ObjectName(refSvc.Name),
+							Namespace: helpers.GetPointer(v1.Namespace(refSvc.Namespace)),
 							Port:      helpers.GetPointer[v1.PortNumber](80),
 						},
 					},
@@ -398,7 +400,7 @@ var _ = Describe("ChangeProcessor", func() {
 
 				routeKey2 = graph.CreateRouteKey(hr2)
 
-				tlsBackendRef := createTLSBackendRef("tls-service", "tls-service-ns")
+				tlsBackendRef := createTLSBackendRef(refTLSSvc.Name, refTLSSvc.Namespace)
 
 				tr1 = createTLSRoute("tr-1", "gateway-1", "foo.tls.com", tlsBackendRef)
 
@@ -562,7 +564,7 @@ var _ = Describe("ChangeProcessor", func() {
 							{
 								BackendRefs: []graph.BackendRef{
 									{
-										SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"},
+										SvcNsName: refSvc,
 										Weight:    1,
 									},
 								},
@@ -642,7 +644,7 @@ var _ = Describe("ChangeProcessor", func() {
 					Spec: graph.L4RouteSpec{
 						Hostnames: tr1.Spec.Hostnames,
 						BackendRef: graph.BackendRef{
-							SvcNsName: types.NamespacedName{Namespace: "tls-service-ns", Name: "tls-service"},
+							SvcNsName: refTLSSvc,
 							Valid:     false,
 						},
 					},
@@ -670,7 +672,7 @@ var _ = Describe("ChangeProcessor", func() {
 					Spec: graph.L4RouteSpec{
 						Hostnames: tr2.Spec.Hostnames,
 						BackendRef: graph.BackendRef{
-							SvcNsName: types.NamespacedName{Namespace: "tls-service-ns", Name: "tls-service"},
+							SvcNsName: refTLSSvc,
 							Valid:     false,
 						},
 					},
@@ -736,15 +738,9 @@ var _ = Describe("ChangeProcessor", func() {
 					L4Routes:          map[graph.L4RouteKey]*graph.L4Route{trKey1: expRouteTR1},
 					Routes:            map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1},
 					ReferencedSecrets: map[types.NamespacedName]*graph.Secret{},
-					ReferencedServices: map[types.NamespacedName]struct{}{
-						{
-							Namespace: "service-ns",
-							Name:      "service",
-						}: {},
-						{
-							Namespace: "tls-service-ns",
-							Name:      "tls-service",
-						}: {},
+					ReferencedServices: map[types.NamespacedName]*graph.ReferencedService{
+						refSvc:    {},
+						refTLSSvc: {},
 					},
 				}
 			})
@@ -953,7 +949,7 @@ var _ = Describe("ChangeProcessor", func() {
 							"Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant",
 						),
 					}
-					delete(expGraph.ReferencedServices, types.NamespacedName{Namespace: "tls-service-ns", Name: "tls-service"})
+					delete(expGraph.ReferencedServices, refTLSSvc)
 					expRouteTR1.Spec.BackendRef.SvcNsName = types.NamespacedName{}
 
 					expGraph.ReferencedSecrets[client.ObjectKeyFromObject(diffNsTLSSecret)] = &graph.Secret{
@@ -1996,11 +1992,13 @@ var _ = Describe("ChangeProcessor", func() {
 
 		Describe("NGF Policy resource changes", Ordered, func() {
 			var (
-				gw              *v1.Gateway
-				route           *v1.HTTPRoute
-				csp, cspUpdated *ngfAPI.ClientSettingsPolicy
-				obs, obsUpdated *ngfAPI.ObservabilityPolicy
-				cspKey, obsKey  graph.PolicyKey
+				gw                     *v1.Gateway
+				route                  *v1.HTTPRoute
+				svc                    *apiv1.Service
+				csp, cspUpdated        *ngfAPI.ClientSettingsPolicy
+				obs, obsUpdated        *ngfAPI.ObservabilityPolicy
+				usp, uspUpdated        *ngfAPI.UpstreamSettingsPolicy
+				cspKey, obsKey, uspKey graph.PolicyKey
 			)
 
 			BeforeAll(func() {
@@ -2011,7 +2009,27 @@ var _ = Describe("ChangeProcessor", func() {
 				Expect(newGraph.NGFPolicies).To(BeEmpty())
 
 				gw = createGateway("gw", createHTTPListener())
-				route = createRoute("hr-1", "gw", "foo.example.com", v1.HTTPBackendRef{})
+				route = createRoute(
+					"hr-1",
+					"gw",
+					"foo.example.com",
+					v1.HTTPBackendRef{
+						BackendRef: v1.BackendRef{
+							BackendObjectReference: v1.BackendObjectReference{
+								Group: helpers.GetPointer[v1.Group](""),
+								Kind:  helpers.GetPointer[v1.Kind](kinds.Service),
+								Name:  "svc",
+								Port:  helpers.GetPointer[v1.PortNumber](80),
+							},
+						},
+					},
+				)
+				svc = &apiv1.Service{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "svc",
+						Namespace: "test",
+					},
+				}
 
 				csp = &ngfAPI.ClientSettingsPolicy{
 					ObjectMeta: metav1.ObjectMeta{
@@ -2072,6 +2090,35 @@ var _ = Describe("ChangeProcessor", func() {
 						Version: "v1alpha1",
 					},
 				}
+
+				usp = &ngfAPI.UpstreamSettingsPolicy{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "usp",
+						Namespace: "test",
+					},
+					Spec: ngfAPI.UpstreamSettingsPolicySpec{
+						ZoneSize: helpers.GetPointer[ngfAPI.Size]("10m"),
+						TargetRefs: []v1alpha2.LocalPolicyTargetReference{
+							{
+								Group: "core",
+								Kind:  kinds.Service,
+								Name:  "svc",
+							},
+						},
+					},
+				}
+
+				uspUpdated = usp.DeepCopy()
+				uspUpdated.Spec.ZoneSize = helpers.GetPointer[ngfAPI.Size]("20m")
+
+				uspKey = graph.PolicyKey{
+					NsName: types.NamespacedName{Name: "usp", Namespace: "test"},
+					GVK: schema.GroupVersionKind{
+						Group:   ngfAPI.GroupName,
+						Kind:    kinds.UpstreamSettingsPolicy,
+						Version: "v1alpha1",
+					},
+				}
 			})
 
 			/*
@@ -2084,6 +2131,7 @@ var _ = Describe("ChangeProcessor", func() {
 				It("reports no changes", func() {
 					processor.CaptureUpsertChange(csp)
 					processor.CaptureUpsertChange(obs)
+					processor.CaptureUpsertChange(usp)
 
 					changed, _ := processor.Process()
 					Expect(changed).To(Equal(state.NoChange))
@@ -2104,12 +2152,19 @@ var _ = Describe("ChangeProcessor", func() {
 					Expect(changed).To(Equal(state.ClusterStateChange))
 					Expect(graph.NGFPolicies).To(HaveKey(obsKey))
 					Expect(graph.NGFPolicies[obsKey].Source).To(Equal(obs))
+
+					processor.CaptureUpsertChange(svc)
+					changed, graph = processor.Process()
+					Expect(changed).To(Equal(state.ClusterStateChange))
+					Expect(graph.NGFPolicies).To(HaveKey(uspKey))
+					Expect(graph.NGFPolicies[uspKey].Source).To(Equal(usp))
 				})
 			})
 			When("the policy is updated", func() {
 				It("captures changes for a policy", func() {
 					processor.CaptureUpsertChange(cspUpdated)
 					processor.CaptureUpsertChange(obsUpdated)
+					processor.CaptureUpsertChange(uspUpdated)
 
 					changed, graph := processor.Process()
 					Expect(changed).To(Equal(state.ClusterStateChange))
@@ -2117,12 +2172,15 @@ var _ = Describe("ChangeProcessor", func() {
 					Expect(graph.NGFPolicies[cspKey].Source).To(Equal(cspUpdated))
 					Expect(graph.NGFPolicies).To(HaveKey(obsKey))
 					Expect(graph.NGFPolicies[obsKey].Source).To(Equal(obsUpdated))
+					Expect(graph.NGFPolicies).To(HaveKey(uspKey))
+					Expect(graph.NGFPolicies[uspKey].Source).To(Equal(uspUpdated))
 				})
 			})
 			When("the policy is deleted", func() {
 				It("removes the policy from the graph", func() {
 					processor.CaptureDeleteChange(&ngfAPI.ClientSettingsPolicy{}, client.ObjectKeyFromObject(csp))
 					processor.CaptureDeleteChange(&ngfAPI.ObservabilityPolicy{}, client.ObjectKeyFromObject(obs))
+					processor.CaptureDeleteChange(&ngfAPI.UpstreamSettingsPolicy{}, client.ObjectKeyFromObject(usp))
 
 					changed, graph := processor.Process()
 					Expect(changed).To(Equal(state.ClusterStateChange))
diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go
index f533b67a1b..cafcbc0db8 100644
--- a/internal/mode/static/state/dataplane/configuration.go
+++ b/internal/mode/static/state/dataplane/configuration.go
@@ -41,12 +41,19 @@ func BuildConfiguration(
 
 	httpServers, sslServers := buildServers(g)
 	backendGroups := buildBackendGroups(append(httpServers, sslServers...))
+	upstreams := buildUpstreams(
+		ctx,
+		g.Gateway.Listeners,
+		serviceResolver,
+		g.ReferencedServices,
+		baseHTTPConfig.IPFamily,
+	)
 
 	config := Configuration{
 		HTTPServers:           httpServers,
 		SSLServers:            sslServers,
 		TLSPassthroughServers: buildPassthroughServers(g),
-		Upstreams:             buildUpstreams(ctx, g.Gateway.Listeners, serviceResolver, baseHTTPConfig.IPFamily),
+		Upstreams:             upstreams,
 		StreamUpstreams:       buildStreamUpstreams(ctx, g.Gateway.Listeners, serviceResolver, baseHTTPConfig.IPFamily),
 		BackendGroups:         backendGroups,
 		SSLKeyPairs:           buildSSLKeyPairs(g.ReferencedSecrets, g.Gateway.Listeners),
@@ -602,6 +609,7 @@ func buildUpstreams(
 	ctx context.Context,
 	listeners []*graph.Listener,
 	svcResolver resolver.ServiceResolver,
+	referencedServices map[types.NamespacedName]*graph.ReferencedService,
 	ipFamily IPFamilyType,
 ) []Upstream {
 	// There can be duplicate upstreams if multiple routes reference the same upstream.
@@ -642,10 +650,16 @@ func buildUpstreams(
 							errMsg = err.Error()
 						}
 
+						var upstreamPolicies []policies.Policy
+						if graphSvc, exists := referencedServices[br.SvcNsName]; exists {
+							upstreamPolicies = buildPolicies(graphSvc.Policies)
+						}
+
 						uniqueUpstreams[upstreamName] = Upstream{
 							Name:      upstreamName,
 							Endpoints: eps,
 							ErrorMsg:  errMsg,
+							Policies:  upstreamPolicies,
 						}
 					}
 				}
diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go
index 7bed89e5d2..037bcd7d9d 100644
--- a/internal/mode/static/state/dataplane/configuration_test.go
+++ b/internal/mode/static/state/dataplane/configuration_test.go
@@ -81,6 +81,7 @@ func getNormalGraph() *graph.Graph {
 		Routes:                     map[graph.RouteKey]*graph.L7Route{},
 		ReferencedSecrets:          map[types.NamespacedName]*graph.Secret{},
 		ReferencedCaCertConfigMaps: map[types.NamespacedName]*graph.CaCertConfigMap{},
+		ReferencedServices:         map[types.NamespacedName]*graph.ReferencedService{},
 	}
 }
 
@@ -2804,6 +2805,13 @@ func TestBuildUpstreams(t *testing.T) {
 		},
 	}
 
+	policyEndpoints := []resolver.Endpoint{
+		{
+			Address: "16.0.0.0",
+			Port:    80,
+		},
+	}
+
 	createBackendRefs := func(serviceNames ...string) []graph.BackendRef {
 		var backends []graph.BackendRef
 		for _, name := range serviceNames {
@@ -2836,6 +2844,8 @@ func TestBuildUpstreams(t *testing.T) {
 
 	invalidHRRefs := createBackendRefs("abc")
 
+	refsWithPolicies := createBackendRefs("policies")
+
 	routes := map[graph.RouteKey]*graph.L7Route{
 		{NamespacedName: types.NamespacedName{Name: "hr1", Namespace: "test"}}: {
 			Valid: true,
@@ -2893,6 +2903,15 @@ func TestBuildUpstreams(t *testing.T) {
 		},
 	}
 
+	routesWithPolicies := map[graph.RouteKey]*graph.L7Route{
+		{NamespacedName: types.NamespacedName{Name: "policies", Namespace: "test"}}: {
+			Valid: true,
+			Spec: graph.L7RouteSpec{
+				Rules: refsToValidRules(refsWithPolicies),
+			},
+		},
+	}
+
 	listeners := []*graph.Listener{
 		{
 			Name:   "invalid-listener",
@@ -2919,6 +2938,41 @@ func TestBuildUpstreams(t *testing.T) {
 			Valid:  true,
 			Routes: routes3,
 		},
+		{
+			Name:   "listener-5",
+			Valid:  true,
+			Routes: routesWithPolicies,
+		},
+	}
+
+	validPolicy1 := &policiesfakes.FakePolicy{}
+	validPolicy2 := &policiesfakes.FakePolicy{}
+	invalidPolicy := &policiesfakes.FakePolicy{}
+
+	referencedServices := map[types.NamespacedName]*graph.ReferencedService{
+		{Name: "bar", Namespace: "test"}:             {},
+		{Name: "baz", Namespace: "test"}:             {},
+		{Name: "baz2", Namespace: "test"}:            {},
+		{Name: "foo", Namespace: "test"}:             {},
+		{Name: "empty-endpoints", Namespace: "test"}: {},
+		{Name: "nil-endpoints", Namespace: "test"}:   {},
+		{Name: "ipv6-endpoints", Namespace: "test"}:  {},
+		{Name: "policies", Namespace: "test"}: {
+			Policies: []*graph.Policy{
+				{
+					Valid:  true,
+					Source: validPolicy1,
+				},
+				{
+					Valid:  false,
+					Source: invalidPolicy,
+				},
+				{
+					Valid:  true,
+					Source: validPolicy2,
+				},
+			},
+		},
 	}
 
 	emptyEndpointsErrMsg := "empty endpoints error"
@@ -2955,6 +3009,11 @@ func TestBuildUpstreams(t *testing.T) {
 			Name:      "test_ipv6-endpoints_80",
 			Endpoints: ipv6Endpoints,
 		},
+		{
+			Name:      "test_policies_80",
+			Endpoints: policyEndpoints,
+			Policies:  []policies.Policy{validPolicy1, validPolicy2},
+		},
 	}
 
 	fakeResolver := &resolverfakes.FakeServiceResolver{}
@@ -2981,6 +3040,8 @@ func TestBuildUpstreams(t *testing.T) {
 			return abcEndpoints, nil
 		case "ipv6-endpoints":
 			return ipv6Endpoints, nil
+		case "policies":
+			return policyEndpoints, nil
 		default:
 			return nil, fmt.Errorf("unexpected service %s", svcNsName.Name)
 		}
@@ -2988,7 +3049,7 @@ func TestBuildUpstreams(t *testing.T) {
 
 	g := NewWithT(t)
 
-	upstreams := buildUpstreams(context.TODO(), listeners, fakeResolver, Dual)
+	upstreams := buildUpstreams(context.TODO(), listeners, fakeResolver, referencedServices, Dual)
 	g.Expect(upstreams).To(ConsistOf(expUpstreams))
 }
 
diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go
index 974efbb142..8075fae374 100644
--- a/internal/mode/static/state/dataplane/types.go
+++ b/internal/mode/static/state/dataplane/types.go
@@ -111,7 +111,7 @@ type Upstream struct {
 	ErrorMsg string
 	// Endpoints are the endpoints of the Upstream.
 	Endpoints []resolver.Endpoint
-	// Policies contains the list of policies that are applied to this Upstream.
+	// Policies holds all the valid policies that apply to the Upstream.
 	Policies []policies.Policy
 }
 
diff --git a/internal/mode/static/state/graph/graph.go b/internal/mode/static/state/graph/graph.go
index 73b2ecac68..0b56ec1018 100644
--- a/internal/mode/static/state/graph/graph.go
+++ b/internal/mode/static/state/graph/graph.go
@@ -66,9 +66,8 @@ type Graph struct {
 	ReferencedSecrets map[types.NamespacedName]*Secret
 	// ReferencedNamespaces includes Namespaces with labels that match the Gateway Listener's label selector.
 	ReferencedNamespaces map[types.NamespacedName]*v1.Namespace
-	// ReferencedServices includes the NamespacedNames of all the Services that are referenced by at least one HTTPRoute.
-	// Storing the whole resource is not necessary, compared to the similar maps above.
-	ReferencedServices map[types.NamespacedName]struct{}
+	// ReferencedServices includes the NamespacedNames of all the Services that are referenced by at least one Route.
+	ReferencedServices map[types.NamespacedName]*ReferencedService
 	// ReferencedCaCertConfigMaps includes ConfigMaps that have been referenced by any BackendTLSPolicies.
 	ReferencedCaCertConfigMaps map[types.NamespacedName]*CaCertConfigMap
 	// BackendTLSPolicies holds BackendTLSPolicy resources.
@@ -155,8 +154,18 @@ func (g *Graph) IsNGFPolicyRelevant(
 	}
 
 	for _, ref := range policy.GetTargetRefs() {
-		if ref.Group == gatewayv1.GroupName && g.gatewayAPIResourceExist(ref, policy.GetNamespace()) {
-			return true
+		switch ref.Group {
+		case gatewayv1.GroupName:
+			if g.gatewayAPIResourceExist(ref, policy.GetNamespace()) {
+				return true
+			}
+		case "", "core":
+			if ref.Kind == kinds.Service {
+				svcNsName := types.NamespacedName{Namespace: policy.GetNamespace(), Name: string(ref.Name)}
+				if _, exists := g.ReferencedServices[svcNsName]; exists {
+					return true
+				}
+			}
 		}
 	}
 
@@ -249,7 +258,7 @@ func BuildGraph(
 
 	referencedNamespaces := buildReferencedNamespaces(state.Namespaces, gw)
 
-	referencedServices := buildReferencedServices(routes, l4routes)
+	referencedServices := buildReferencedServices(routes, l4routes, gw)
 
 	// policies must be processed last because they rely on the state of the other resources in the graph
 	processedPolicies := processPolicies(
@@ -257,6 +266,7 @@ func BuildGraph(
 		validators.PolicyValidator,
 		processedGws,
 		routes,
+		referencedServices,
 		globalSettings,
 	)
 
diff --git a/internal/mode/static/state/graph/graph_test.go b/internal/mode/static/state/graph/graph_test.go
index 627e308bc0..f2b71d05f8 100644
--- a/internal/mode/static/state/graph/graph_test.go
+++ b/internal/mode/static/state/graph/graph_test.go
@@ -33,7 +33,6 @@ func TestBuildGraph(t *testing.T) {
 	const (
 		gcName         = "my-class"
 		controllerName = "my.controller"
-		testNS         = "test"
 	)
 
 	protectedPorts := ProtectedPorts{
@@ -894,7 +893,7 @@ func TestBuildGraph(t *testing.T) {
 			ReferencedNamespaces: map[types.NamespacedName]*v1.Namespace{
 				client.ObjectKeyFromObject(ns): ns,
 			},
-			ReferencedServices: map[types.NamespacedName]struct{}{
+			ReferencedServices: map[types.NamespacedName]*ReferencedService{
 				client.ObjectKeyFromObject(svc):  {},
 				client.ObjectKeyFromObject(svc1): {},
 			},
@@ -1157,7 +1156,7 @@ func TestIsReferenced(t *testing.T) {
 		ReferencedNamespaces: map[types.NamespacedName]*v1.Namespace{
 			client.ObjectKeyFromObject(nsInGraph): nsInGraph,
 		},
-		ReferencedServices: map[types.NamespacedName]struct{}{
+		ReferencedServices: map[types.NamespacedName]*ReferencedService{
 			client.ObjectKeyFromObject(serviceInGraph): {},
 		},
 		ReferencedCaCertConfigMaps: map[types.NamespacedName]*CaCertConfigMap{
@@ -1359,6 +1358,7 @@ func TestIsNGFPolicyRelevant(t *testing.T) {
 					Source: &policiesfakes.FakePolicy{},
 				},
 			},
+			ReferencedServices: nil,
 		}
 	}
 
@@ -1469,6 +1469,46 @@ func TestIsNGFPolicyRelevant(t *testing.T) {
 			nsname:      types.NamespacedName{Namespace: "test", Name: "nil-gw-source"},
 			expRelevant: false,
 		},
+		{
+			name: "relevant; policy references a Service that is referenced by a route, group core is inferred",
+			graph: getModifiedGraph(func(g *Graph) *Graph {
+				g.ReferencedServices = map[types.NamespacedName]*ReferencedService{
+					{Namespace: "test", Name: "ref-service"}: {},
+				}
+
+				return g
+			}),
+			policy:      getPolicy(createTestRef(kinds.Service, "", "ref-service")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "policy-for-svc"},
+			expRelevant: true,
+		},
+		{
+			name: "relevant; policy references a Service that is referenced by a route, group core is explicit",
+			graph: getModifiedGraph(func(g *Graph) *Graph {
+				g.ReferencedServices = map[types.NamespacedName]*ReferencedService{
+					{Namespace: "test", Name: "ref-service"}: {},
+				}
+
+				return g
+			}),
+			policy:      getPolicy(createTestRef(kinds.Service, "core", "ref-service")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "policy-for-svc"},
+			expRelevant: true,
+		},
+		{
+			name:        "irrelevant; policy references a Service that is not referenced by a route, group core is inferred",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef(kinds.Service, "", "not-ref-service")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "policy-for-not-ref-svc"},
+			expRelevant: false,
+		},
+		{
+			name:        "irrelevant; policy references a Service that is not referenced by a route, group core is explicit",
+			graph:       getGraph(),
+			policy:      getPolicy(createTestRef(kinds.Service, "core", "not-ref-service")),
+			nsname:      types.NamespacedName{Namespace: "test", Name: "policy-for-not-ref-svc"},
+			expRelevant: false,
+		},
 	}
 
 	for _, test := range tests {
diff --git a/internal/mode/static/state/graph/policies.go b/internal/mode/static/state/graph/policies.go
index a4b794c5e1..9ce3e7eb23 100644
--- a/internal/mode/static/state/graph/policies.go
+++ b/internal/mode/static/state/graph/policies.go
@@ -21,7 +21,7 @@ import (
 type Policy struct {
 	// Source is the corresponding Policy resource.
 	Source policies.Policy
-	// Ancestors is list of ancestor objects of the Policy. Used in status.
+	// Ancestors is a list of ancestor objects of the Policy. Used in status.
 	Ancestors []PolicyAncestor
 	// TargetRefs are the resources that the Policy targets.
 	TargetRefs []PolicyTargetRef
@@ -63,6 +63,7 @@ const (
 	gatewayGroupKind = v1.GroupName + "/" + kinds.Gateway
 	hrGroupKind      = v1.GroupName + "/" + kinds.HTTPRoute
 	grpcGroupKind    = v1.GroupName + "/" + kinds.GRPCRoute
+	serviceGroupKind = "core" + "/" + kinds.Service
 )
 
 // attachPolicies attaches the graph's processed policies to the resources they target. It modifies the graph in place.
@@ -83,11 +84,42 @@ func (g *Graph) attachPolicies(ctlrName string) {
 				}
 
 				attachPolicyToRoute(policy, route, ctlrName)
+			case kinds.Service:
+				svc, exists := g.ReferencedServices[ref.Nsname]
+				if !exists {
+					continue
+				}
+
+				attachPolicyToService(policy, svc, g.Gateway, ctlrName)
 			}
 		}
 	}
 }
 
+func attachPolicyToService(
+	policy *Policy,
+	svc *ReferencedService,
+	gw *Gateway,
+	ctlrName string,
+) {
+	if ngfPolicyAncestorsFull(policy, ctlrName) {
+		return
+	}
+
+	ancestor := PolicyAncestor{
+		Ancestor: createParentReference(v1.GroupName, kinds.Gateway, client.ObjectKeyFromObject(gw.Source)),
+	}
+
+	if !gw.Valid {
+		ancestor.Conditions = []conditions.Condition{staticConds.NewPolicyTargetNotFound("Parent Gateway is invalid")}
+		policy.Ancestors = append(policy.Ancestors, ancestor)
+		return
+	}
+
+	policy.Ancestors = append(policy.Ancestors, ancestor)
+	svc.Policies = append(svc.Policies, policy)
+}
+
 func attachPolicyToRoute(policy *Policy, route *L7Route, ctlrName string) {
 	kind := v1.Kind(kinds.HTTPRoute)
 	if route.RouteType == RouteTypeGRPC {
@@ -158,6 +190,7 @@ func processPolicies(
 	validator validation.PolicyValidator,
 	gateways processedGateways,
 	routes map[RouteKey]*L7Route,
+	services map[types.NamespacedName]*ReferencedService,
 	globalSettings *policies.GlobalSettings,
 ) map[PolicyKey]*Policy {
 	if len(pols) == 0 || gateways.Winner == nil {
@@ -174,9 +207,8 @@ func processPolicies(
 
 		for _, ref := range policy.GetTargetRefs() {
 			refNsName := types.NamespacedName{Name: string(ref.Name), Namespace: policy.GetNamespace()}
-			refGroupKind := fmt.Sprintf("%s/%s", ref.Group, ref.Kind)
 
-			switch refGroupKind {
+			switch refGroupKind(ref.Group, ref.Kind) {
 			case gatewayGroupKind:
 				if !gatewayExists(refNsName, gateways.Winner, gateways.Ignored) {
 					continue
@@ -187,6 +219,10 @@ func processPolicies(
 				} else {
 					targetedRoutes[client.ObjectKeyFromObject(route.Source)] = route
 				}
+			case serviceGroupKind:
+				if _, exists := services[refNsName]; !exists {
+					continue
+				}
 			default:
 				continue
 			}
@@ -203,22 +239,8 @@ func processPolicies(
 			continue
 		}
 
-		for _, targetedRoute := range targetedRoutes {
-			// We need to check if this route referenced in the policy has an overlapping
-			// hostname:port/path with any other route that isn't referenced by this policy.
-			// If so, deny the policy.
-			hostPortPaths := buildHostPortPaths(targetedRoute)
-
-			for _, route := range routes {
-				if _, ok := targetedRoutes[client.ObjectKeyFromObject(route.Source)]; ok {
-					continue
-				}
-
-				if cond := checkForRouteOverlap(route, hostPortPaths); cond != nil {
-					conds = append(conds, *cond)
-				}
-			}
-		}
+		overlapConds := checkTargetRoutesForOverlap(targetedRoutes, routes)
+		conds = append(conds, overlapConds...)
 
 		conds = append(conds, validator.Validate(policy, globalSettings)...)
 
@@ -236,6 +258,32 @@ func processPolicies(
 	return processedPolicies
 }
 
+func checkTargetRoutesForOverlap(
+	targetedRoutes map[types.NamespacedName]*L7Route,
+	graphRoutes map[RouteKey]*L7Route,
+) []conditions.Condition {
+	var conds []conditions.Condition
+
+	for _, targetedRoute := range targetedRoutes {
+		// We need to check if this route referenced in the policy has an overlapping
+		// hostname:port/path with any other route that isn't referenced by this policy.
+		// If so, deny the policy.
+		hostPortPaths := buildHostPortPaths(targetedRoute)
+
+		for _, route := range graphRoutes {
+			if _, ok := targetedRoutes[client.ObjectKeyFromObject(route.Source)]; ok {
+				continue
+			}
+
+			if cond := checkForRouteOverlap(route, hostPortPaths); cond != nil {
+				conds = append(conds, *cond)
+			}
+		}
+	}
+
+	return conds
+}
+
 // checkForRouteOverlap checks if any route references the same hostname:port/path combination
 // as a route referenced in a policy.
 func checkForRouteOverlap(route *L7Route, hostPortPaths map[string]string) *conditions.Condition {
@@ -355,3 +403,12 @@ func markConflictedPolicies(pols map[PolicyKey]*Policy, validator validation.Pol
 		}
 	}
 }
+
+// refGroupKind formats the group and kind as a string.
+func refGroupKind(group v1.Group, kind v1.Kind) string {
+	if group == "" {
+		return fmt.Sprintf("core/%s", kind)
+	}
+
+	return fmt.Sprintf("%s/%s", group, kind)
+}
diff --git a/internal/mode/static/state/graph/policies_test.go b/internal/mode/static/state/graph/policies_test.go
index 340d3f10f0..2c11ac2229 100644
--- a/internal/mode/static/state/graph/policies_test.go
+++ b/internal/mode/static/state/graph/policies_test.go
@@ -15,7 +15,7 @@ import (
 	"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/policies"
-	policiesfakes "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies/policiesfakes"
+	"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/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"
 )
@@ -24,7 +24,9 @@ var testNs = "test"
 
 func TestAttachPolicies(t *testing.T) {
 	t.Parallel()
+
 	policyGVK := schema.GroupVersionKind{Group: "Group", Version: "Version", Kind: "Policy"}
+
 	createPolicy := func(targetRefsNames []string, refKind v1.Kind) *Policy {
 		targetRefs := make([]PolicyTargetRef, 0, len(targetRefsNames))
 		for _, name := range targetRefsNames {
@@ -48,18 +50,6 @@ func TestAttachPolicies(t *testing.T) {
 		}
 	}
 
-	createGateway := func(name string) *Gateway {
-		return &Gateway{
-			Source: &v1.Gateway{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      name,
-					Namespace: testNs,
-				},
-			},
-			Valid: true,
-		}
-	}
-
 	createRoutesForGraph := func(routes map[string]RouteType) map[RouteKey]*L7Route {
 		routesMap := make(map[RouteKey]*L7Route, len(routes))
 		for routeName, routeType := range routes {
@@ -84,94 +74,137 @@ func TestAttachPolicies(t *testing.T) {
 		return routesMap
 	}
 
-	expectNoPolicyAttachment := func(g *WithT, graph *Graph) {
+	expectNoGatewayPolicyAttachment := func(g *WithT, graph *Graph) {
 		if graph.Gateway != nil {
 			g.Expect(graph.Gateway.Policies).To(BeNil())
 		}
+	}
 
+	expectNoRoutePolicyAttachment := func(g *WithT, graph *Graph) {
 		for _, r := range graph.Routes {
 			g.Expect(r.Policies).To(BeNil())
 		}
 	}
 
-	expectPolicyAttachment := func(g *WithT, graph *Graph) {
+	expectNoSvcPolicyAttachment := func(g *WithT, graph *Graph) {
+		for _, r := range graph.ReferencedServices {
+			g.Expect(r.Policies).To(BeNil())
+		}
+	}
+
+	expectGatewayPolicyAttachment := func(g *WithT, graph *Graph) {
 		if graph.Gateway != nil {
 			g.Expect(graph.Gateway.Policies).To(HaveLen(1))
 		}
+	}
 
+	expectRoutePolicyAttachment := func(g *WithT, graph *Graph) {
 		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))
+	expectSvcPolicyAttachment := func(g *WithT, graph *Graph) {
+		for _, r := range graph.ReferencedServices {
+			g.Expect(r.Policies).To(HaveLen(1))
 		}
+	}
 
-		for _, r := range graph.Routes {
-			g.Expect(r.Policies).To(BeNil())
+	expectNoAttachmentList := []func(g *WithT, graph *Graph){
+		expectNoGatewayPolicyAttachment,
+		expectNoSvcPolicyAttachment,
+		expectNoRoutePolicyAttachment,
+	}
+
+	expectAllAttachmentList := []func(g *WithT, graph *Graph){
+		expectGatewayPolicyAttachment,
+		expectSvcPolicyAttachment,
+		expectRoutePolicyAttachment,
+	}
+
+	getPolicies := func() map[PolicyKey]*Policy {
+		return map[PolicyKey]*Policy{
+			createTestPolicyKey(policyGVK, "gw-policy1"): createPolicy([]string{"gateway", "gateway1"}, kinds.Gateway),
+			createTestPolicyKey(policyGVK, "route-policy1"): createPolicy(
+				[]string{"hr1-route", "hr2-route"},
+				kinds.HTTPRoute,
+			),
+			createTestPolicyKey(policyGVK, "grpc-route-policy1"): createPolicy([]string{"grpc-route"}, kinds.GRPCRoute),
+			createTestPolicyKey(policyGVK, "svc-policy"):         createPolicy([]string{"svc-1"}, kinds.Service),
+		}
+	}
+
+	getRoutes := func() map[RouteKey]*L7Route {
+		return createRoutesForGraph(
+			map[string]RouteType{
+				"hr1-route":  RouteTypeHTTP,
+				"hr2-route":  RouteTypeHTTP,
+				"grpc-route": RouteTypeGRPC,
+			},
+		)
+	}
+
+	getGateway := func() *Gateway {
+		return &Gateway{
+			Source: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "gateway",
+					Namespace: testNs,
+				},
+			},
+			Valid: true,
+		}
+	}
+
+	getServices := func() map[types.NamespacedName]*ReferencedService {
+		return map[types.NamespacedName]*ReferencedService{
+			{Namespace: testNs, Name: "svc-1"}: {},
 		}
 	}
 
 	tests := []struct {
 		gateway     *Gateway
 		routes      map[RouteKey]*L7Route
+		svcs        map[types.NamespacedName]*ReferencedService
 		ngfPolicies map[PolicyKey]*Policy
-		expect      func(g *WithT, graph *Graph)
 		name        string
+		expects     []func(g *WithT, graph *Graph)
 	}{
 		{
-			name: "nil Gateway",
-			routes: createRoutesForGraph(
-				map[string]RouteType{
-					"hr1-route":  RouteTypeHTTP,
-					"hr2-route":  RouteTypeHTTP,
-					"grpc-route": RouteTypeGRPC,
-				},
-			),
-			ngfPolicies: map[PolicyKey]*Policy{
-				createTestPolicyKey(policyGVK, "gw-policy"): createPolicy([]string{"gateway", "gateway1"}, kinds.Gateway),
-				createTestPolicyKey(policyGVK, "route-policy"): createPolicy(
-					[]string{"hr1-route", "hr2-route"},
-					kinds.HTTPRoute,
-				),
-				createTestPolicyKey(policyGVK, "grpc-route-policy"): createPolicy([]string{"grpc-route"}, kinds.GRPCRoute),
-			},
-			expect: expectNoPolicyAttachment,
+			name:        "nil Gateway; no policies attach",
+			routes:      getRoutes(),
+			ngfPolicies: getPolicies(),
+			expects:     expectNoAttachmentList,
 		},
 		{
-			name:    "nil routes",
-			gateway: createGateway("gateway"),
-			ngfPolicies: map[PolicyKey]*Policy{
-				createTestPolicyKey(policyGVK, "gw-policy1"): createPolicy([]string{"gateway", "gateway1"}, kinds.Gateway),
-				createTestPolicyKey(policyGVK, "route-policy1"): createPolicy(
-					[]string{"hr1-route", "hr2-route"},
-					kinds.HTTPRoute,
-				),
-				createTestPolicyKey(policyGVK, "grpc-route-policy1"): createPolicy([]string{"grpc-route"}, kinds.GRPCRoute),
+			name:        "nil Routes; gateway and service policies attach",
+			gateway:     getGateway(),
+			svcs:        getServices(),
+			ngfPolicies: getPolicies(),
+			expects: []func(g *WithT, graph *Graph){
+				expectGatewayPolicyAttachment,
+				expectSvcPolicyAttachment,
+				expectNoRoutePolicyAttachment,
 			},
-			expect: expectGatewayPolicyAttachment,
 		},
 		{
-			name: "normal",
-			routes: createRoutesForGraph(
-				map[string]RouteType{
-					"hr-1":   RouteTypeHTTP,
-					"hr-2":   RouteTypeHTTP,
-					"grpc-1": RouteTypeGRPC,
-				},
-			),
-			ngfPolicies: map[PolicyKey]*Policy{
-				createTestPolicyKey(policyGVK, "gw-policy2"): createPolicy([]string{"gateway2", "gateway3"}, kinds.Gateway),
-				createTestPolicyKey(policyGVK, "route-policy2"): createPolicy(
-					[]string{"hr-1", "hr-2"},
-					kinds.HTTPRoute,
-				),
-				createTestPolicyKey(policyGVK, "grpc-route-policy2"): createPolicy([]string{"grpc-1"}, kinds.GRPCRoute),
+			name:        "nil ReferencedServices; gateway and route policies attach",
+			routes:      getRoutes(),
+			ngfPolicies: getPolicies(),
+			gateway:     getGateway(),
+			expects: []func(g *WithT, graph *Graph){
+				expectGatewayPolicyAttachment,
+				expectRoutePolicyAttachment,
+				expectNoSvcPolicyAttachment,
 			},
-			gateway: createGateway("gateway2"),
-			expect:  expectPolicyAttachment,
+		},
+		{
+			name:        "all policies attach",
+			routes:      getRoutes(),
+			svcs:        getServices(),
+			ngfPolicies: getPolicies(),
+			gateway:     getGateway(),
+			expects:     expectAllAttachmentList,
 		},
 	}
 
@@ -181,13 +214,16 @@ func TestAttachPolicies(t *testing.T) {
 			g := NewWithT(t)
 
 			graph := &Graph{
-				Gateway:     test.gateway,
-				Routes:      test.routes,
-				NGFPolicies: test.ngfPolicies,
+				Gateway:            test.gateway,
+				Routes:             test.routes,
+				ReferencedServices: test.svcs,
+				NGFPolicies:        test.ngfPolicies,
 			}
 
 			graph.attachPolicies("nginx-gateway")
-			test.expect(g, graph)
+			for _, expect := range test.expects {
+				expect(g, graph)
+			}
 		})
 	}
 }
@@ -360,15 +396,6 @@ func TestAttachPolicyToGateway(t *testing.T) {
 		}
 	}
 
-	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
@@ -508,6 +535,83 @@ func TestAttachPolicyToGateway(t *testing.T) {
 	}
 }
 
+func TestAttachPolicyToService(t *testing.T) {
+	t.Parallel()
+
+	gwNsname := types.NamespacedName{Namespace: testNs, Name: "gateway"}
+
+	getGateway := func(valid bool) *Gateway {
+		return &Gateway{
+			Source: &v1.Gateway{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      gwNsname.Name,
+					Namespace: gwNsname.Namespace,
+				},
+			},
+			Valid: valid,
+		}
+	}
+
+	tests := []struct {
+		policy       *Policy
+		svc          *ReferencedService
+		gw           *Gateway
+		name         string
+		expAncestors []PolicyAncestor
+		expAttached  bool
+	}{
+		{
+			name:        "attachment",
+			policy:      &Policy{Source: &policiesfakes.FakePolicy{}},
+			svc:         &ReferencedService{},
+			gw:          getGateway(true /*valid*/),
+			expAttached: true,
+			expAncestors: []PolicyAncestor{
+				{
+					Ancestor: getGatewayParentRef(gwNsname),
+				},
+			},
+		},
+		{
+			name:        "no attachment; gateway is invalid",
+			policy:      &Policy{Source: &policiesfakes.FakePolicy{}},
+			svc:         &ReferencedService{},
+			gw:          getGateway(false /*invalid*/),
+			expAttached: false,
+			expAncestors: []PolicyAncestor{
+				{
+					Ancestor:   getGatewayParentRef(gwNsname),
+					Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("Parent Gateway is invalid")},
+				},
+			},
+		},
+		{
+			name:         "no attachment; max ancestor",
+			policy:       &Policy{Source: createTestPolicyWithAncestors(16)},
+			svc:          &ReferencedService{},
+			gw:           getGateway(true /*valid*/),
+			expAttached:  false,
+			expAncestors: nil,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			t.Parallel()
+			g := NewWithT(t)
+
+			attachPolicyToService(test.policy, test.svc, test.gw, "ctlr")
+			if test.expAttached {
+				g.Expect(test.svc.Policies).To(HaveLen(1))
+			} else {
+				g.Expect(test.svc.Policies).To(BeEmpty())
+			}
+
+			g.Expect(test.policy.Ancestors).To(BeEquivalentTo(test.expAncestors))
+		})
+	}
+}
+
 func TestProcessPolicies(t *testing.T) {
 	t.Parallel()
 	policyGVK := schema.GroupVersionKind{Group: "Group", Version: "Version", Kind: "MyPolicy"}
@@ -518,6 +622,7 @@ func TestProcessPolicies(t *testing.T) {
 	grpcRef := createTestRef(kinds.GRPCRoute, v1.GroupName, "grpc")
 	gatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "gw")
 	ignoredGatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "ignored")
+	svcRef := createTestRef(kinds.Service, "core", "svc")
 
 	// These refs reference objects that do not belong to NGF.
 	// Policies that contain these refs should NOT be processed.
@@ -525,6 +630,7 @@ func TestProcessPolicies(t *testing.T) {
 	hrWrongGroup := createTestRef(kinds.HTTPRoute, "WrongGroup", "hr")
 	gatewayWrongGroupRef := createTestRef(kinds.Gateway, "WrongGroup", "gw")
 	nonNGFGatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "not-ours")
+	svcDoesNotExistRef := createTestRef(kinds.Service, "core", "dne")
 
 	pol1, pol1Key := createTestPolicyAndKey(policyGVK, "pol1", hrRef)
 	pol2, pol2Key := createTestPolicyAndKey(policyGVK, "pol2", grpcRef)
@@ -534,6 +640,8 @@ func TestProcessPolicies(t *testing.T) {
 	pol6, pol6Key := createTestPolicyAndKey(policyGVK, "pol6", hrWrongGroup)
 	pol7, pol7Key := createTestPolicyAndKey(policyGVK, "pol7", gatewayWrongGroupRef)
 	pol8, pol8Key := createTestPolicyAndKey(policyGVK, "pol8", nonNGFGatewayRef)
+	pol9, pol9Key := createTestPolicyAndKey(policyGVK, "pol9", svcDoesNotExistRef)
+	pol10, pol10Key := createTestPolicyAndKey(policyGVK, "pol10", svcRef)
 
 	pol1Conflict, pol1ConflictKey := createTestPolicyAndKey(policyGVK, "pol1-conflict", hrRef)
 
@@ -553,14 +661,16 @@ func TestProcessPolicies(t *testing.T) {
 			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,
+				pol1Key:  pol1,
+				pol2Key:  pol2,
+				pol3Key:  pol3,
+				pol4Key:  pol4,
+				pol5Key:  pol5,
+				pol6Key:  pol6,
+				pol7Key:  pol7,
+				pol8Key:  pol8,
+				pol9Key:  pol9,
+				pol10Key: pol10,
 			},
 			expProcessedPolicies: map[PolicyKey]*Policy{
 				pol1Key: {
@@ -611,6 +721,18 @@ func TestProcessPolicies(t *testing.T) {
 					Ancestors: []PolicyAncestor{},
 					Valid:     true,
 				},
+				pol10Key: {
+					Source: pol10,
+					TargetRefs: []PolicyTargetRef{
+						{
+							Nsname: types.NamespacedName{Namespace: testNs, Name: "svc"},
+							Kind:   kinds.Service,
+							Group:  "core",
+						},
+					},
+					Ancestors: []PolicyAncestor{},
+					Valid:     true,
+				},
 			},
 		},
 		{
@@ -740,12 +862,16 @@ func TestProcessPolicies(t *testing.T) {
 		},
 	}
 
+	services := map[types.NamespacedName]*ReferencedService{
+		{Namespace: testNs, Name: "svc"}: {},
+	}
+
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			t.Parallel()
 			g := NewWithT(t)
 
-			processed := processPolicies(test.policies, test.validator, gateways, routes, nil)
+			processed := processPolicies(test.policies, test.validator, gateways, routes, services, nil)
 			g.Expect(processed).To(BeEquivalentTo(test.expProcessedPolicies))
 		})
 	}
@@ -885,7 +1011,7 @@ func TestProcessPolicies_RouteOverlap(t *testing.T) {
 			t.Parallel()
 			g := NewWithT(t)
 
-			processed := processPolicies(test.policies, test.validator, gateways, test.routes, nil)
+			processed := processPolicies(test.policies, test.validator, gateways, test.routes, nil, nil)
 			g.Expect(processed).To(HaveLen(1))
 
 			for _, pol := range processed {
@@ -1051,6 +1177,45 @@ func TestMarkConflictedPolicies(t *testing.T) {
 	}
 }
 
+func TestRefGroupKind(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name      string
+		group     v1.Group
+		kind      v1.Kind
+		expString string
+	}{
+		{
+			name:      "explicit group core",
+			group:     "core",
+			kind:      kinds.Service,
+			expString: "core/Service",
+		},
+		{
+			name:      "implicit group core",
+			group:     "",
+			kind:      kinds.Service,
+			expString: "core/Service",
+		},
+		{
+			name:      "gateway group",
+			group:     v1.GroupName,
+			kind:      kinds.HTTPRoute,
+			expString: "gateway.networking.k8s.io/HTTPRoute",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			t.Parallel()
+			g := NewWithT(t)
+
+			g.Expect(refGroupKind(test.group, test.kind)).To(Equal(test.expString))
+		})
+	}
+}
+
 func createTestPolicyWithAncestors(numAncestors int) policies.Policy {
 	policy := &policiesfakes.FakePolicy{}
 
@@ -1151,3 +1316,12 @@ func createTestRouteWithPaths(name string, paths ...string) *L7Route {
 
 	return route
 }
+
+func getGatewayParentRef(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),
+	}
+}
diff --git a/internal/mode/static/state/graph/service.go b/internal/mode/static/state/graph/service.go
index ad33579f1e..ad6fb817ef 100644
--- a/internal/mode/static/state/graph/service.go
+++ b/internal/mode/static/state/graph/service.go
@@ -2,17 +2,31 @@ package graph
 
 import (
 	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+// A ReferencedService represents a Kubernetes Service that is referenced by a Route and that belongs to the
+// winning Gateway. It does not contain the v1.Service object, because Services are resolved when building
+// the dataplane.Configuration.
+type ReferencedService struct {
+	// Policies is a list of NGF Policies that target this Service.
+	Policies []*Policy
+}
+
 func buildReferencedServices(
 	l7routes map[RouteKey]*L7Route,
 	l4Routes map[L4RouteKey]*L4Route,
-) map[types.NamespacedName]struct{} {
-	svcNames := make(map[types.NamespacedName]struct{})
+	gw *Gateway,
+) map[types.NamespacedName]*ReferencedService {
+	if gw == nil {
+		return nil
+	}
 
-	attached := func(parentRefs []ParentRef) bool {
-		for _, ref := range parentRefs {
-			if ref.Attachment.Attached {
+	referencedServices := make(map[types.NamespacedName]*ReferencedService)
+
+	belongsToWinningGw := func(refs []ParentRef) bool {
+		for _, ref := range refs {
+			if ref.Gateway == client.ObjectKeyFromObject(gw.Source) {
 				return true
 			}
 		}
@@ -22,21 +36,24 @@ func buildReferencedServices(
 
 	// Processes both valid and invalid BackendRefs as invalid ones still have referenced services
 	// we may want to track.
-
-	populateServiceNamesForL7Routes := func(routeRules []RouteRule) {
+	addServicesForL7Routes := func(routeRules []RouteRule) {
 		for _, rule := range routeRules {
 			for _, ref := range rule.BackendRefs {
 				if ref.SvcNsName != (types.NamespacedName{}) {
-					svcNames[ref.SvcNsName] = struct{}{}
+					referencedServices[ref.SvcNsName] = &ReferencedService{
+						Policies: nil,
+					}
 				}
 			}
 		}
 	}
 
-	populateServiceNamesForL4Routes := func(route *L4Route) {
+	addServicesForL4Routes := func(route *L4Route) {
 		nsname := route.Spec.BackendRef.SvcNsName
 		if nsname != (types.NamespacedName{}) {
-			svcNames[nsname] = struct{}{}
+			referencedServices[nsname] = &ReferencedService{
+				Policies: nil,
+			}
 		}
 	}
 
@@ -48,12 +65,11 @@ func buildReferencedServices(
 			continue
 		}
 
-		// If none of the ParentRefs are attached to the Gateway, we want to skip the route.
-		if !attached(route.ParentRefs) {
+		if !belongsToWinningGw(route.ParentRefs) {
 			continue
 		}
 
-		populateServiceNamesForL7Routes(route.Spec.Rules)
+		addServicesForL7Routes(route.Spec.Rules)
 	}
 
 	for _, route := range l4Routes {
@@ -61,16 +77,16 @@ func buildReferencedServices(
 			continue
 		}
 
-		// If none of the ParentRefs are attached to the Gateway, we want to skip the route.
-		if !attached(route.ParentRefs) {
+		if !belongsToWinningGw(route.ParentRefs) {
 			continue
 		}
 
-		populateServiceNamesForL4Routes(route)
+		addServicesForL4Routes(route)
 	}
 
-	if len(svcNames) == 0 {
+	if len(referencedServices) == 0 {
 		return nil
 	}
-	return svcNames
+
+	return referencedServices
 }
diff --git a/internal/mode/static/state/graph/service_test.go b/internal/mode/static/state/graph/service_test.go
index 5c60831a31..0fa316e73f 100644
--- a/internal/mode/static/state/graph/service_test.go
+++ b/internal/mode/static/state/graph/service_test.go
@@ -4,18 +4,30 @@ import (
 	"testing"
 
 	. "github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+	v1 "sigs.k8s.io/gateway-api/apis/v1"
 )
 
 func TestBuildReferencedServices(t *testing.T) {
 	t.Parallel()
+
+	gwNsname := types.NamespacedName{Namespace: "test", Name: "gwNsname"}
+	gw := &Gateway{
+		Source: &v1.Gateway{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: gwNsname.Namespace,
+				Name:      gwNsname.Name,
+			},
+		},
+	}
+	ignoredGw := types.NamespacedName{Namespace: "test", Name: "ignoredGw"}
+
 	getNormalL7Route := func() *L7Route {
 		return &L7Route{
 			ParentRefs: []ParentRef{
 				{
-					Attachment: &ParentRefAttachmentStatus{
-						Attached: true,
-					},
+					Gateway: gwNsname,
 				},
 			},
 			Valid: true,
@@ -48,9 +60,7 @@ func TestBuildReferencedServices(t *testing.T) {
 			Valid: true,
 			ParentRefs: []ParentRef{
 				{
-					Attachment: &ParentRefAttachmentStatus{
-						Attached: true,
-					},
+					Gateway: gwNsname,
 				},
 			},
 		}
@@ -117,111 +127,102 @@ func TestBuildReferencedServices(t *testing.T) {
 		return route
 	})
 
-	unattachedRoute := getModifiedL7Route(func(route *L7Route) *L7Route {
-		route.ParentRefs[0].Attachment.Attached = false
+	validRouteNoServiceNsName := getModifiedL7Route(func(route *L7Route) *L7Route {
+		route.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{}
 		return route
 	})
 
-	unattachedL4Route := getModifiedL4Route(func(route *L4Route) *L4Route {
-		route.ParentRefs[0].Attachment.Attached = false
+	validL4RouteNoServiceNsName := getModifiedL4Route(func(route *L4Route) *L4Route {
+		route.Spec.BackendRef.SvcNsName = types.NamespacedName{}
 		return route
 	})
 
-	attachedRouteWithManyParentRefs := getModifiedL7Route(func(route *L7Route) *L7Route {
+	normalL4RouteWinningAndIgnoredGws := getModifiedL4Route(func(route *L4Route) *L4Route {
 		route.ParentRefs = []ParentRef{
 			{
-				Attachment: &ParentRefAttachmentStatus{
-					Attached: false,
-				},
+				Gateway: ignoredGw,
 			},
 			{
-				Attachment: &ParentRefAttachmentStatus{
-					Attached: false,
-				},
+				Gateway: ignoredGw,
 			},
 			{
-				Attachment: &ParentRefAttachmentStatus{
-					Attached: true,
-				},
+				Gateway: gwNsname,
 			},
 		}
-
 		return route
 	})
 
-	attachedL4RoutesWithManyParentRefs := getModifiedL4Route(func(route *L4Route) *L4Route {
+	normalRouteWinningAndIgnoredGws := getModifiedL7Route(func(route *L7Route) *L7Route {
 		route.ParentRefs = []ParentRef{
 			{
-				Attachment: &ParentRefAttachmentStatus{
-					Attached: false,
-				},
+				Gateway: ignoredGw,
 			},
 			{
-				Attachment: &ParentRefAttachmentStatus{
-					Attached: true,
-				},
+				Gateway: gwNsname,
 			},
 			{
-				Attachment: &ParentRefAttachmentStatus{
-					Attached: false,
-				},
+				Gateway: ignoredGw,
 			},
 		}
-
 		return route
 	})
 
-	validRouteNoServiceNsName := getModifiedL7Route(func(route *L7Route) *L7Route {
-		route.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{}
+	normalL4RouteIgnoredGw := getModifiedL4Route(func(route *L4Route) *L4Route {
+		route.ParentRefs[0].Gateway = ignoredGw
 		return route
 	})
 
-	validL4RouteNoServiceNsName := getModifiedL4Route(func(route *L4Route) *L4Route {
-		route.Spec.BackendRef.SvcNsName = types.NamespacedName{}
+	normalL7RouteIgnoredGw := getModifiedL7Route(func(route *L7Route) *L7Route {
+		route.ParentRefs[0].Gateway = ignoredGw
 		return route
 	})
 
 	tests := []struct {
 		l7Routes map[RouteKey]*L7Route
 		l4Routes map[L4RouteKey]*L4Route
-		exp      map[types.NamespacedName]struct{}
+		exp      map[types.NamespacedName]*ReferencedService
+		gw       *Gateway
 		name     string
 	}{
 		{
 			name: "normal routes",
+			gw:   gw,
 			l7Routes: map[RouteKey]*L7Route{
 				{NamespacedName: types.NamespacedName{Name: "normal-route"}}: normalRoute,
 			},
 			l4Routes: map[L4RouteKey]*L4Route{
 				{NamespacedName: types.NamespacedName{Name: "normal-l4-route"}}: normalL4Route,
 			},
-			exp: map[types.NamespacedName]struct{}{
+			exp: map[types.NamespacedName]*ReferencedService{
 				{Namespace: "banana-ns", Name: "service"}:   {},
 				{Namespace: "tlsroute-ns", Name: "service"}: {},
 			},
 		},
 		{
 			name: "l7 route with two services in one Rule", // l4 routes don't support multiple services right now
+			gw:   gw,
 			l7Routes: map[RouteKey]*L7Route{
 				{NamespacedName: types.NamespacedName{Name: "two-svc-one-rule"}}: validRouteTwoServicesOneRule,
 			},
-			exp: map[types.NamespacedName]struct{}{
+			exp: map[types.NamespacedName]*ReferencedService{
 				{Namespace: "service-ns", Name: "service"}:   {},
 				{Namespace: "service-ns2", Name: "service2"}: {},
 			},
 		},
 		{
 			name: "route with one service per rule", // l4 routes don't support multiple rules right now
+			gw:   gw,
 			l7Routes: map[RouteKey]*L7Route{
 				{NamespacedName: types.NamespacedName{Name: "one-svc-per-rule"}}: validRouteTwoServicesTwoRules,
 			},
-			exp: map[types.NamespacedName]struct{}{
+			exp: map[types.NamespacedName]*ReferencedService{
 				{Namespace: "service-ns", Name: "service"}:   {},
 				{Namespace: "service-ns2", Name: "service2"}: {},
 			},
 		},
 		{
 			name: "multiple valid routes with same services",
+			gw:   gw,
 			l7Routes: map[RouteKey]*L7Route{
 				{NamespacedName: types.NamespacedName{Name: "one-svc-per-rule"}}: validRouteTwoServicesTwoRules,
 				{NamespacedName: types.NamespacedName{Name: "two-svc-one-rule"}}: validRouteTwoServicesOneRule,
@@ -231,15 +232,41 @@ func TestBuildReferencedServices(t *testing.T) {
 				{NamespacedName: types.NamespacedName{Name: "l4-route-2"}}:                    normalL4Route2,
 				{NamespacedName: types.NamespacedName{Name: "l4-route-same-svc-as-l7-route"}}: normalL4RouteWithSameSvcAsL7Route,
 			},
-			exp: map[types.NamespacedName]struct{}{
+			exp: map[types.NamespacedName]*ReferencedService{
 				{Namespace: "service-ns", Name: "service"}:   {},
 				{Namespace: "service-ns2", Name: "service2"}: {},
 				{Namespace: "tlsroute-ns", Name: "service"}:  {},
 				{Namespace: "tlsroute-ns", Name: "service2"}: {},
 			},
 		},
+		{
+			name: "valid routes that do not belong to winning gateway",
+			gw:   gw,
+			l7Routes: map[RouteKey]*L7Route{
+				{NamespacedName: types.NamespacedName{Name: "belongs-to-ignored-gws"}}: normalL7RouteIgnoredGw,
+			},
+			l4Routes: map[L4RouteKey]*L4Route{
+				{NamespacedName: types.NamespacedName{Name: "belongs-to-ignored-gw"}}: normalL4RouteIgnoredGw,
+			},
+			exp: nil,
+		},
+		{
+			name: "valid routes that belong to both winning and ignored gateways",
+			gw:   gw,
+			l7Routes: map[RouteKey]*L7Route{
+				{NamespacedName: types.NamespacedName{Name: "belongs-to-ignored-gws"}}: normalRouteWinningAndIgnoredGws,
+			},
+			l4Routes: map[L4RouteKey]*L4Route{
+				{NamespacedName: types.NamespacedName{Name: "ignored-gw"}}: normalL4RouteWinningAndIgnoredGws,
+			},
+			exp: map[types.NamespacedName]*ReferencedService{
+				{Namespace: "banana-ns", Name: "service"}:   {},
+				{Namespace: "tlsroute-ns", Name: "service"}: {},
+			},
+		},
 		{
 			name: "valid routes with different services",
+			gw:   gw,
 			l7Routes: map[RouteKey]*L7Route{
 				{NamespacedName: types.NamespacedName{Name: "one-svc-per-rule"}}: validRouteTwoServicesTwoRules,
 				{NamespacedName: types.NamespacedName{Name: "normal-route"}}:     normalRoute,
@@ -247,7 +274,7 @@ func TestBuildReferencedServices(t *testing.T) {
 			l4Routes: map[L4RouteKey]*L4Route{
 				{NamespacedName: types.NamespacedName{Name: "normal-l4-route"}}: normalL4Route,
 			},
-			exp: map[types.NamespacedName]struct{}{
+			exp: map[types.NamespacedName]*ReferencedService{
 				{Namespace: "service-ns", Name: "service"}:   {},
 				{Namespace: "service-ns2", Name: "service2"}: {},
 				{Namespace: "banana-ns", Name: "service"}:    {},
@@ -256,6 +283,7 @@ func TestBuildReferencedServices(t *testing.T) {
 		},
 		{
 			name: "invalid routes",
+			gw:   gw,
 			l7Routes: map[RouteKey]*L7Route{
 				{NamespacedName: types.NamespacedName{Name: "invalid-route"}}: invalidRoute,
 			},
@@ -264,18 +292,9 @@ func TestBuildReferencedServices(t *testing.T) {
 			},
 			exp: nil,
 		},
-		{
-			name: "unattached route",
-			l7Routes: map[RouteKey]*L7Route{
-				{NamespacedName: types.NamespacedName{Name: "unattached-route"}}: unattachedRoute,
-			},
-			l4Routes: map[L4RouteKey]*L4Route{
-				{NamespacedName: types.NamespacedName{Name: "unattached-l4-route"}}: unattachedL4Route,
-			},
-			exp: nil,
-		},
 		{
 			name: "combination of valid and invalid routes",
+			gw:   gw,
 			l7Routes: map[RouteKey]*L7Route{
 				{NamespacedName: types.NamespacedName{Name: "normal-route"}}:  normalRoute,
 				{NamespacedName: types.NamespacedName{Name: "invalid-route"}}: invalidRoute,
@@ -284,26 +303,25 @@ func TestBuildReferencedServices(t *testing.T) {
 				{NamespacedName: types.NamespacedName{Name: "invalid-l4-route"}}: invalidL4Route,
 				{NamespacedName: types.NamespacedName{Name: "normal-l4-route"}}:  normalL4Route,
 			},
-			exp: map[types.NamespacedName]struct{}{
+			exp: map[types.NamespacedName]*ReferencedService{
 				{Namespace: "banana-ns", Name: "service"}:   {},
 				{Namespace: "tlsroute-ns", Name: "service"}: {},
 			},
 		},
 		{
-			name: "route with many parentRefs and one is attached",
+			name: "valid route no service nsname",
+			gw:   gw,
 			l7Routes: map[RouteKey]*L7Route{
-				{NamespacedName: types.NamespacedName{Name: "multiple-parent-ref-route"}}: attachedRouteWithManyParentRefs,
+				{NamespacedName: types.NamespacedName{Name: "no-service-nsname"}}: validRouteNoServiceNsName,
 			},
 			l4Routes: map[L4RouteKey]*L4Route{
-				{NamespacedName: types.NamespacedName{Name: "multiple-parent-ref-l4-route"}}: attachedL4RoutesWithManyParentRefs,
-			},
-			exp: map[types.NamespacedName]struct{}{
-				{Namespace: "banana-ns", Name: "service"}:   {},
-				{Namespace: "tlsroute-ns", Name: "service"}: {},
+				{NamespacedName: types.NamespacedName{Name: "no-service-nsname-l4"}}: validL4RouteNoServiceNsName,
 			},
+			exp: nil,
 		},
 		{
-			name: "valid route no service nsname",
+			name: "nil gateway",
+			gw:   nil,
 			l7Routes: map[RouteKey]*L7Route{
 				{NamespacedName: types.NamespacedName{Name: "no-service-nsname"}}: validRouteNoServiceNsName,
 			},
@@ -318,7 +336,8 @@ func TestBuildReferencedServices(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			t.Parallel()
 			g := NewWithT(t)
-			g.Expect(buildReferencedServices(test.l7Routes, test.l4Routes)).To(Equal(test.exp))
+
+			g.Expect(buildReferencedServices(test.l7Routes, test.l4Routes, test.gw)).To(Equal(test.exp))
 		})
 	}
 }
diff --git a/internal/mode/static/telemetry/collector_test.go b/internal/mode/static/telemetry/collector_test.go
index 59bee05aae..3128531ebb 100644
--- a/internal/mode/static/telemetry/collector_test.go
+++ b/internal/mode/static/telemetry/collector_test.go
@@ -302,7 +302,7 @@ var _ = Describe("Collector", Ordered, func() {
 						},
 						client.ObjectKeyFromObject(nilsecret): nil,
 					},
-					ReferencedServices: map[types.NamespacedName]struct{}{
+					ReferencedServices: map[types.NamespacedName]*graph.ReferencedService{
 						client.ObjectKeyFromObject(svc1):   {},
 						client.ObjectKeyFromObject(svc2):   {},
 						client.ObjectKeyFromObject(nilsvc): {},
@@ -583,7 +583,7 @@ var _ = Describe("Collector", Ordered, func() {
 						Source: secret,
 					},
 				},
-				ReferencedServices: map[types.NamespacedName]struct{}{
+				ReferencedServices: map[types.NamespacedName]*graph.ReferencedService{
 					client.ObjectKeyFromObject(svc): {},
 				},
 				NGFPolicies: map[graph.PolicyKey]*graph.Policy{