Repository: Kong/kubernetes-ingress-controller (go)

Issue:
Unable to set apply hashOnFallback set to cookie when using `KongUpstreamPolicy`
### Is there an existing issue for this?

- [x] I have searched the existing issues

### Current Behavior

The following `KongIngress` manifest

```
apiVersion: configuration.konghq.com/v1
kind: KongIngress
metadata:
  name: cookie-fallback
  namespace: kong
upstream:
  hash_on: header
  hash_on_header: x-lb
  hash_fallback: cookie
  hash_on_cookie: test
  algorithm: consistent-hashing
```

can't be migrated to `KongUpstreamPolicy` e.g.

```
apiVersion: configuration.konghq.com/v1beta1
kind: KongUpstreamPolicy
metadata:
  name: kongupstream
spec:
  hashOn:
    header: x-lb
  hashOnFallback:
    cookie: test
    cookiePath: /
  algorithm: consistent-hashing
```

because it fails to apply:

```
The KongUpstreamPolicy "kongupstream" is invalid:
* <nil>: Invalid value: "object": spec.hashOnFallback.cookie must not be set.
* <nil>: Invalid value: "object": spec.hashOnFallback.cookiePath must not be set.
```

### Expected Behavior

### Steps To Reproduce

### Kong Ingress Controller version

```shell
3.4.7
```

### Anything else?

https://github.com/Kong/kubernetes-configuration/pull/498 should fix the CRD CEL validation rules but there's still an issue with applying the hash on setting with `cookie` in KIC's translation logic: https://github.com/Kong/kubernetes-ingress-controller/blob/c46527e778a5efd89821537a4371657032c8d20b/internal/dataplane/kongstate/kongupstreampolicy.go#L75-L85

x-ref [FTI-6800](https://konghq.atlassian.net/browse/FTI-6800)

[FTI-6800]: https://konghq.atlassian.net/browse/FTI-6800?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Pull request description:
fix: fix KongUpstreamPolicy hash on translation
**What this PR does / why we need it**:

Fix `KongUpstreamPolicy` hash on translation.

Currently when users specify cookie as hash fallback the rules are not translated correctly due to peculiar field usage in kong upstream (and `KongUpstreamHash`): cookie and cookie path is used both for hash_on and hash_fallback  in kong upstream but not in `KongUpstreamHash` so we can't simply look at hash_on to set the `KongUpstreamHash`'s `HashOn` to decide how to translate the config.

The way this PR changes the translation, is to retain the "no error" policy, so we rely on CRD validation rules (to be changed in https://github.com/Kong/kubernetes-configuration/pull/498).

In case somehow the invalid config gets pushed to Kong, we are generating `KongConfigurationApplyFailed` events and that's the case when `hash_fallback` is used when cookie is the primary hashing mechanism (docs: https://developer.konghq.com/gateway/entities/upstream/#consistent-hashing).

**Which issue this PR fixes**:

Fixes #7580

FTI-6800

<!--
Here you can add any links to issues that this PR is relevant for.
You can use Github keywords (like: closes, fixes or resolves) to auto-resolve
the linked issue(s) when this PR gets merged.

For example: fixes #<issue number>
-->

**Special notes for your reviewer**:

This currently uses a commit from release/1.5.x branch [be1d520ac78bc4d46d4335b425ab8e521488316a](https://github.com/Kong/kubernetes-configuration/commit/be1d520ac78bc4d46d4335b425ab8e521488316a). ~https://github.com/Kong/kubernetes-configuration/tree/fix-kongupstreampolicy-hash-on-validation-1.5 branch which has https://github.com/Kong/kubernetes-configuration/pull/498 manually cherry picked.~

Please review together with https://github.com/Kong/kubernetes-configuration/pull/499 which targets the 1.5 release branch of kubernetes-configuration (currently used in KIC).

**PR Readiness Checklist**:

Complete these before marking the PR as `ready to review`:

- [x] the `CHANGELOG.md` release notes have been updated to reflect any significant (and particularly user-facing) changes introduced by this PR


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99638dfcb..6528659db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -149,7 +149,7 @@ Adding a new version? You'll need three changes:
 
 ### Fixed
 
-- When synchronization of license with Konnect is enabled 
+- When synchronization of license with Konnect is enabled
   (`--konnect-licensing-enabled` is `true`), do not mark the pod as ready
   until there is an available license fetched from Konnect or stored in the
   `Secret`.
@@ -157,7 +157,11 @@ Adding a new version? You'll need three changes:
 - Reject `HTTPRoute` with filters that have unsupported types in validating
   webhook. This fixed the issue where an `HTTPRoute` with `CORS` filter is
   accepted by the validating webhook.
-  [#7582](https://github.com/Kong/kubernetes-ingress-controller/pull/7582) 
+  [#7582](https://github.com/Kong/kubernetes-ingress-controller/pull/7582)
+- Fix `KongUpstreamPolicy` hash on fallback configuration so that cookie hashing
+  can be used for both hash_on (always) and hash_fallack (when primary hashing
+  source is different than cookie).
+ [#7582](https://github.com/Kong/kubernetes-ingress-controller/pull/7582)
 
 ## [3.4.7]
 
@@ -520,7 +524,7 @@ Please use [3.4.1] or later instead.
 - It is now possible to disable synchronization of consumers to Konnect through the
   flag `--konnect-disable-consumers-sync`.
   [#6313](https://github.com/Kong/kubernetes-ingress-controller/pull/6313)
-- Allow `KongCustomEntity` to refer to plugins in another namespace via 
+- Allow `KongCustomEntity` to refer to plugins in another namespace via
   `spec.parentRef.namespace`. The reference is allowed only when there is a
   `ReferenceGrant` in the namespace of the `KongPlugin` to grant permissions
   to `KongCustomEntity` of referring to `KongPlugin`.
@@ -781,7 +785,7 @@ Please use [3.4.1] or later instead.
 - Do not generate invalid duplicate upstream targets when routes use multiple
   Services with the same endpoints.
   [#5817](https://github.com/Kong/kubernetes-ingress-controller/pull/5817)
-- Remove the constraint of items of `parentRefs` can only be empty or 
+- Remove the constraint of items of `parentRefs` can only be empty or
   `gateway.network.k8s.io/Gateway` in validating `HTTPRoute`s. If an item in
   `parentRefs`'s group/kind is not `gateway.network.k8s.io/Gateway`, the item
   is seen as a parent other than the controller and ignored in parentRef check.
@@ -827,7 +831,6 @@ Please use [3.4.1] or later instead.
   attached to the route or service only.
   [#6132](https://github.com/Kong/kubernetes-ingress-controller/pull/6132)
 
-
 ## [3.1.5]
 
 > Release date: 2024-05-17
@@ -992,7 +995,7 @@ Please use [3.4.1] or later instead.
   [#5312](https://github.com/Kong/kubernetes-ingress-controller/pull/5312)
 - Added functionality to the `KongUpstreamPolicy` controller to properly set and
   enforce `KongUpstreamPolicy` status. 
-  The controller will set an ancestor status in `KongUpstreamPolicy` status for 
+  The controller will set an ancestor status in `KongUpstreamPolicy` status for
   each of its ancestors (i.e. `Service` or `KongServiceFacade`) with the `Accepted`
   and `Programmed` condition.
   [#5185](https://github.com/Kong/kubernetes-ingress-controller/pull/5185)
diff --git a/Makefile b/Makefile
index 5700414f6..951493867 100644
--- a/Makefile
+++ b/Makefile
@@ -781,9 +781,11 @@ uninstall-gateway-api-crds: go-mod-download-gateway-api kustomize
 	$(KUSTOMIZE) build $(GATEWAY_API_CRDS_LOCAL_PATH) | kubectl delete -f -
 	$(KUSTOMIZE) build $(GATEWAY_API_CRDS_LOCAL_PATH)/experimental | kubectl delete -f -
 
-# Install CRDs into the K8s cluster specified in $KUBECONFIG.
 .PHONY: install
-install: manifests install-gateway-api-crds
+install: manifests install-gateway-api-crds install.crds.kubernetes-configuration
+
+.PHONY: install.crds.kubernetes-configuration
+install.crds.kubernetes-configuration: kustomize
 	$(KUSTOMIZE) build config/crd | kubectl apply -f -
 
 # Uninstall CRDs from the K8s cluster specified in $KUBECONFIG.
diff --git a/config/crd/incubator/kustomization.yaml b/config/crd/incubator/kustomization.yaml
index 0ae321219..5c15ed4fb 100644
--- a/config/crd/incubator/kustomization.yaml
+++ b/config/crd/incubator/kustomization.yaml
@@ -2,4 +2,4 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
-  - https://github.com/kong/kubernetes-configuration/config/crd/ingress-controller-incubator?ref=v1.5.1 # Version is auto-updated by the generating script.
+  - https://github.com/kong/kubernetes-configuration/config/crd/ingress-controller-incubator?ref=be1d520ac78bc4d46d4335b425ab8e521488316a # Version is auto-updated by the generating script.
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index f2325c98a..373c931a0 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -2,4 +2,4 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
-  - https://github.com/kong/kubernetes-configuration/config/crd/ingress-controller?ref=v1.5.1 # Version is auto-updated by the generating script.
+  - https://github.com/kong/kubernetes-configuration/config/crd/ingress-controller?ref=be1d520ac78bc4d46d4335b425ab8e521488316a # Version is auto-updated by the generating script.
diff --git a/docs/api-reference.md b/docs/api-reference.md
index 6de505437..79cfb31d3 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -747,7 +747,7 @@ Only one of the fields must be set.
 
 | Field | Description |
 | --- | --- |
-| `input` _[HashInput](#hashinput)_ | Input allows using one of the predefined inputs (ip, consumer, path, none). Set this to `none` if you want to use sticky sessions. For other parametrized inputs, use one of the fields below. |
+| `input` _[HashInput](#hashinput)_ | Input allows using one of the predefined inputs (ip, consumer, path, none). Set this to `none` if you want to use sticky sessions. For other parameterized inputs, use one of the fields below. |
 | `header` _string_ | Header is the name of the header to use as hash input. |
 | `cookie` _string_ | Cookie is the name of the cookie to use as hash input. |
 | `cookiePath` _string_ | CookiePath is cookie path to set in the response headers. |
diff --git a/go.mod b/go.mod
index ba51a1a49..96acf46af 100644
--- a/go.mod
+++ b/go.mod
@@ -32,7 +32,7 @@ require (
 	github.com/jpillora/backoff v1.0.0
 	github.com/kong/go-database-reconciler v1.24.2
 	github.com/kong/go-kong v0.67.0
-	github.com/kong/kubernetes-configuration v1.5.1
+	github.com/kong/kubernetes-configuration v1.5.2-0.20250703130217-be1d520ac78b
 	github.com/kong/kubernetes-telemetry v0.1.10
 	github.com/kong/kubernetes-testing-framework v0.47.2
 	github.com/lithammer/dedent v1.1.0
diff --git a/go.sum b/go.sum
index 70160fc69..073285a83 100644
--- a/go.sum
+++ b/go.sum
@@ -230,8 +230,8 @@ github.com/kong/go-database-reconciler v1.24.2 h1:TNY4dy+uJcmSlWAElye65eVl1Wd1xg
 github.com/kong/go-database-reconciler v1.24.2/go.mod h1:fWjwkA2MDQu1QKLV8qeUCagGmOU4wNhPAQkl5LBfYd0=
 github.com/kong/go-kong v0.67.0 h1:54zXKc58IZpZdlJCv8p95SJjejTxT+cwbWXw97icCak=
 github.com/kong/go-kong v0.67.0/go.mod h1:J0vGB3wsZ2i99zly1zTRe3v7rOKpkhQZRwbcTFP76qM=
-github.com/kong/kubernetes-configuration v1.5.1 h1:MVhZBXQA1v5qbHAsnC831OhCd41nYrCzkWJtXugjtGE=
-github.com/kong/kubernetes-configuration v1.5.1/go.mod h1:DuPOhI2Ljm0EFE/rlL21DiURiqrcnc2Fs/rxVmeWNd0=
+github.com/kong/kubernetes-configuration v1.5.2-0.20250703130217-be1d520ac78b h1:T9UW9T9m7yIiljEvWeHCrhbAPSXKEVVr+8ScgsRklDo=
+github.com/kong/kubernetes-configuration v1.5.2-0.20250703130217-be1d520ac78b/go.mod h1:DuPOhI2Ljm0EFE/rlL21DiURiqrcnc2Fs/rxVmeWNd0=
 github.com/kong/kubernetes-telemetry v0.1.10 h1:V+/Lco8VFY/CzoELwOPcGTyg0zbj/HAvoFkH50UsKYs=
 github.com/kong/kubernetes-telemetry v0.1.10/go.mod h1:/r/FevTOGegCqaxXCJyGkbE1E3IcYUGJd7xX7D73s2Y=
 github.com/kong/kubernetes-testing-framework v0.47.2 h1:+2Z9anTpbV/hwNeN+NFQz53BMU+g3QJydkweBp3tULo=
diff --git a/internal/dataplane/kongstate/kongupstreampolicy.go b/internal/dataplane/kongstate/kongupstreampolicy.go
index 72f5c3377..51b8869d2 100644
--- a/internal/dataplane/kongstate/kongupstreampolicy.go
+++ b/internal/dataplane/kongstate/kongupstreampolicy.go
@@ -75,8 +75,8 @@ func TranslateKongUpstreamPolicy(policy configurationv1beta1.KongUpstreamPolicyS
 		HashOn:           translateHashOn(policy.HashOn),
 		HashOnHeader:     translateHashOnHeader(policy.HashOn),
 		HashOnURICapture: translateHashOnURICapture(policy.HashOn),
-		HashOnCookie:     translateHashOnCookie(policy.HashOn),
-		HashOnCookiePath: translateHashOnCookiePath(policy.HashOn),
+		HashOnCookie:     translateHashOnCookie(policy.HashOn, policy.HashOnFallback),
+		HashOnCookiePath: translateHashOnCookiePath(policy.HashOn, policy.HashOnFallback),
 		HashOnQueryArg:   translateHashOnQueryArg(policy.HashOn),
 
 		HashFallback:           translateHashOn(policy.HashOnFallback),
@@ -93,7 +93,10 @@ func translateHashOn(hashOn *configurationv1beta1.KongUpstreamHash) *string {
 	if hashOn == nil {
 		return nil
 	}
-	// CRD validations will ensure only one of hashOn fields can be set, therefore the order doesn't matter.
+
+	// CRD validations will ensure only one of hashOn fields can be set, therefore
+	// the order in which we check these doesn't matter.
+
 	switch {
 	case hashOn.Input != nil:
 		return lo.ToPtr(string(*hashOn.Input))
@@ -117,11 +120,20 @@ func translateHashOnHeader(hashOn *configurationv1beta1.KongUpstreamHash) *strin
 	return hashOn.Header
 }
 
-func translateHashOnCookie(hashOn *configurationv1beta1.KongUpstreamHash) *string {
-	if hashOn == nil {
+func translateHashOnCookie(hashOn, hashOnFallback *configurationv1beta1.KongUpstreamHash) *string {
+	if hashOn == nil && hashOnFallback == nil {
 		return nil
 	}
-	return hashOn.Cookie
+
+	if hashOn != nil && hashOn.Cookie != nil {
+		return hashOn.Cookie
+	}
+	if hashOnFallback != nil && hashOnFallback.Cookie != nil {
+		return hashOnFallback.Cookie
+	}
+
+	// This should not happen as this should be prevented by the CRD validation.
+	return nil
 }
 
 func translateHashOnQueryArg(hashOn *configurationv1beta1.KongUpstreamHash) *string {
@@ -138,11 +150,20 @@ func translateHashOnURICapture(hashOn *configurationv1beta1.KongUpstreamHash) *s
 	return hashOn.URICapture
 }
 
-func translateHashOnCookiePath(hashOn *configurationv1beta1.KongUpstreamHash) *string {
-	if hashOn == nil {
+func translateHashOnCookiePath(hashOn, hashOnFallback *configurationv1beta1.KongUpstreamHash) *string {
+	if hashOn == nil && hashOnFallback == nil {
 		return nil
 	}
-	return hashOn.CookiePath
+
+	if hashOn != nil && hashOn.CookiePath != nil {
+		return hashOn.CookiePath
+	}
+	if hashOnFallback != nil && hashOnFallback.CookiePath != nil {
+		return hashOnFallback.CookiePath
+	}
+
+	// This should not happen as this should be prevented by the CRD validation.
+	return nil
 }
 
 func translateHealthchecks(healthchecks *configurationv1beta1.KongUpstreamHealthcheck) *kong.Healthcheck {


Test patch:
diff --git a/internal/dataplane/kongstate/kongupstreampolicy_test.go b/internal/dataplane/kongstate/kongupstreampolicy_test.go
index 784380d2b..875b42a02 100644
--- a/internal/dataplane/kongstate/kongupstreampolicy_test.go
+++ b/internal/dataplane/kongstate/kongupstreampolicy_test.go
@@ -193,6 +193,109 @@ func TestTranslateKongUpstreamPolicy(t *testing.T) {
 				HashFallbackHeader: lo.ToPtr("bar"),
 			},
 		},
+		{
+			name: "KongUpstreamPolicySpec with hash-on header and hash-on fallback cookie",
+			policySpec: configurationv1beta1.KongUpstreamPolicySpec{
+				HashOn: &configurationv1beta1.KongUpstreamHash{
+					Header: lo.ToPtr("foo"),
+				},
+				HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+					Cookie:     lo.ToPtr("cookie-name"),
+					CookiePath: lo.ToPtr("/cookie-path"),
+				},
+			},
+			expectedUpstream: &kong.Upstream{
+				HashOn:           lo.ToPtr("header"),
+				HashOnHeader:     lo.ToPtr("foo"),
+				HashFallback:     lo.ToPtr("cookie"),
+				HashOnCookie:     lo.ToPtr("cookie-name"),
+				HashOnCookiePath: lo.ToPtr("/cookie-path"),
+			},
+		},
+		{
+			name: "KongUpstreamPolicySpec with hash-on query-arg and hash-on fallback cookie",
+			policySpec: configurationv1beta1.KongUpstreamPolicySpec{
+				HashOn: &configurationv1beta1.KongUpstreamHash{
+					QueryArg: lo.ToPtr("foo"),
+				},
+				HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+					Cookie:     lo.ToPtr("cookie-name"),
+					CookiePath: lo.ToPtr("/cookie-path"),
+				},
+			},
+			expectedUpstream: &kong.Upstream{
+				HashOn:           lo.ToPtr("query_arg"),
+				HashOnQueryArg:   lo.ToPtr("foo"),
+				HashFallback:     lo.ToPtr("cookie"),
+				HashOnCookie:     lo.ToPtr("cookie-name"),
+				HashOnCookiePath: lo.ToPtr("/cookie-path"),
+			},
+		},
+		{
+			name: "KongUpstreamPolicySpec with hash-on uri-capture and hash-on fallback cookie",
+			policySpec: configurationv1beta1.KongUpstreamPolicySpec{
+				HashOn: &configurationv1beta1.KongUpstreamHash{
+					URICapture: lo.ToPtr("foo"),
+				},
+				HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+					Cookie:     lo.ToPtr("cookie-name"),
+					CookiePath: lo.ToPtr("/cookie-path"),
+				},
+			},
+			expectedUpstream: &kong.Upstream{
+				HashOn:           lo.ToPtr("uri_capture"),
+				HashOnURICapture: lo.ToPtr("foo"),
+				HashFallback:     lo.ToPtr("cookie"),
+				HashOnCookie:     lo.ToPtr("cookie-name"),
+				HashOnCookiePath: lo.ToPtr("/cookie-path"),
+			},
+		},
+		{
+			// This will be blocked by CRD validation rules because according to
+			// https://developer.konghq.com/gateway/entities/upstream/#consistent-hashing
+			// if the primary hash_on is set to cookie, the hash_fallback is invalid
+			// and cannot be used.
+			name: "KongUpstreamPolicySpec with hash-on cookie and hash-on fallback cookie is incorrect and should not happen",
+			policySpec: configurationv1beta1.KongUpstreamPolicySpec{
+				HashOn: &configurationv1beta1.KongUpstreamHash{
+					Cookie:     lo.ToPtr("cookie-name"),
+					CookiePath: lo.ToPtr("/cookie-path"),
+				},
+				HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+					Cookie:     lo.ToPtr("cookie-name-2"),
+					CookiePath: lo.ToPtr("/cookie-path-2"),
+				},
+			},
+			expectedUpstream: &kong.Upstream{
+				HashOn:           lo.ToPtr("cookie"),
+				HashOnCookie:     lo.ToPtr("cookie-name"),
+				HashOnCookiePath: lo.ToPtr("/cookie-path"),
+				HashFallback:     lo.ToPtr("cookie"),
+			},
+		},
+		{
+			// This will be blocked by CRD validation rules because according to
+			// https://developer.konghq.com/gateway/entities/upstream/#consistent-hashing
+			// if the primary hash_on is set to cookie, the hash_fallback is invalid
+			// and cannot be used.
+			name: "KongUpstreamPolicySpec with hash-on cookie and hash-on fallback header is incorrect and should not happen",
+			policySpec: configurationv1beta1.KongUpstreamPolicySpec{
+				HashOn: &configurationv1beta1.KongUpstreamHash{
+					Cookie:     lo.ToPtr("cookie-name"),
+					CookiePath: lo.ToPtr("/cookie-path"),
+				},
+				HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+					Header: lo.ToPtr("header-name"),
+				},
+			},
+			expectedUpstream: &kong.Upstream{
+				HashOn:             lo.ToPtr("cookie"),
+				HashOnCookie:       lo.ToPtr("cookie-name"),
+				HashOnCookiePath:   lo.ToPtr("/cookie-path"),
+				HashFallback:       lo.ToPtr("header"),
+				HashFallbackHeader: lo.ToPtr("header-name"),
+			},
+		},
 		{
 			name: "KongUpstreamPolicySpec with hash-on cookie",
 			policySpec: configurationv1beta1.KongUpstreamPolicySpec{
diff --git a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
index a01d60332..f0163c04a 100644
--- a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
@@ -2242,7 +2242,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2278,7 +2278,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2836,11 +2836,9 @@ spec:
             is set.
           rule: 'has(self.spec.hashOnFallback) ? has(self.spec.algorithm) && self.spec.algorithm
             == "consistent-hashing" : true'
-        - message: spec.hashOnFallback.cookie must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookie)
-            : true'
-        - message: spec.hashOnFallback.cookiePath must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookiePath)
+        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
+            set.
+          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
             : true'
         - message: spec.healthchecks.passive.healthy.interval must not be set.
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
@@ -2850,10 +2848,6 @@ spec:
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
             && has(self.spec.healthchecks.passive.unhealthy) ? !has(self.spec.healthchecks.passive.unhealthy.interval)
             : true'
-        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
-            set.
-          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
-            : true'
         - message: When spec.stickySessions is set, spec.hashOn.input must be set
             to 'none' (no other hash_on fields allowed).
           rule: 'has(self.spec.stickySessions) ? (has(self.spec.hashOn) && has(self.spec.hashOn.input)
diff --git a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
index d82aea0e0..4b5192278 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
@@ -2242,7 +2242,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2278,7 +2278,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2836,11 +2836,9 @@ spec:
             is set.
           rule: 'has(self.spec.hashOnFallback) ? has(self.spec.algorithm) && self.spec.algorithm
             == "consistent-hashing" : true'
-        - message: spec.hashOnFallback.cookie must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookie)
-            : true'
-        - message: spec.hashOnFallback.cookiePath must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookiePath)
+        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
+            set.
+          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
             : true'
         - message: spec.healthchecks.passive.healthy.interval must not be set.
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
@@ -2850,10 +2848,6 @@ spec:
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
             && has(self.spec.healthchecks.passive.unhealthy) ? !has(self.spec.healthchecks.passive.unhealthy.interval)
             : true'
-        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
-            set.
-          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
-            : true'
         - message: When spec.stickySessions is set, spec.hashOn.input must be set
             to 'none' (no other hash_on fields allowed).
           rule: 'has(self.spec.stickySessions) ? (has(self.spec.hashOn) && has(self.spec.hashOn.input)
diff --git a/test/e2e/manifests/all-in-one-dbless-konnect.yaml b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
index 65bef2354..a2b4b072a 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
@@ -2242,7 +2242,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2278,7 +2278,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2836,11 +2836,9 @@ spec:
             is set.
           rule: 'has(self.spec.hashOnFallback) ? has(self.spec.algorithm) && self.spec.algorithm
             == "consistent-hashing" : true'
-        - message: spec.hashOnFallback.cookie must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookie)
-            : true'
-        - message: spec.hashOnFallback.cookiePath must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookiePath)
+        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
+            set.
+          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
             : true'
         - message: spec.healthchecks.passive.healthy.interval must not be set.
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
@@ -2850,10 +2848,6 @@ spec:
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
             && has(self.spec.healthchecks.passive.unhealthy) ? !has(self.spec.healthchecks.passive.unhealthy.interval)
             : true'
-        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
-            set.
-          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
-            : true'
         - message: When spec.stickySessions is set, spec.hashOn.input must be set
             to 'none' (no other hash_on fields allowed).
           rule: 'has(self.spec.stickySessions) ? (has(self.spec.hashOn) && has(self.spec.hashOn.input)
diff --git a/test/e2e/manifests/all-in-one-dbless.yaml b/test/e2e/manifests/all-in-one-dbless.yaml
index caf287501..38f5d645f 100644
--- a/test/e2e/manifests/all-in-one-dbless.yaml
+++ b/test/e2e/manifests/all-in-one-dbless.yaml
@@ -2242,7 +2242,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2278,7 +2278,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2836,11 +2836,9 @@ spec:
             is set.
           rule: 'has(self.spec.hashOnFallback) ? has(self.spec.algorithm) && self.spec.algorithm
             == "consistent-hashing" : true'
-        - message: spec.hashOnFallback.cookie must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookie)
-            : true'
-        - message: spec.hashOnFallback.cookiePath must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookiePath)
+        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
+            set.
+          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
             : true'
         - message: spec.healthchecks.passive.healthy.interval must not be set.
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
@@ -2850,10 +2848,6 @@ spec:
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
             && has(self.spec.healthchecks.passive.unhealthy) ? !has(self.spec.healthchecks.passive.unhealthy.interval)
             : true'
-        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
-            set.
-          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
-            : true'
         - message: When spec.stickySessions is set, spec.hashOn.input must be set
             to 'none' (no other hash_on fields allowed).
           rule: 'has(self.spec.stickySessions) ? (has(self.spec.hashOn) && has(self.spec.hashOn.input)
diff --git a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
index b77643082..4c9af4bf2 100644
--- a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
@@ -2242,7 +2242,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2278,7 +2278,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2836,11 +2836,9 @@ spec:
             is set.
           rule: 'has(self.spec.hashOnFallback) ? has(self.spec.algorithm) && self.spec.algorithm
             == "consistent-hashing" : true'
-        - message: spec.hashOnFallback.cookie must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookie)
-            : true'
-        - message: spec.hashOnFallback.cookiePath must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookiePath)
+        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
+            set.
+          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
             : true'
         - message: spec.healthchecks.passive.healthy.interval must not be set.
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
@@ -2850,10 +2848,6 @@ spec:
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
             && has(self.spec.healthchecks.passive.unhealthy) ? !has(self.spec.healthchecks.passive.unhealthy.interval)
             : true'
-        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
-            set.
-          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
-            : true'
         - message: When spec.stickySessions is set, spec.hashOn.input must be set
             to 'none' (no other hash_on fields allowed).
           rule: 'has(self.spec.stickySessions) ? (has(self.spec.hashOn) && has(self.spec.hashOn.input)
diff --git a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
index 86d4b9631..30496ad9d 100644
--- a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
@@ -2242,7 +2242,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2278,7 +2278,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2836,11 +2836,9 @@ spec:
             is set.
           rule: 'has(self.spec.hashOnFallback) ? has(self.spec.algorithm) && self.spec.algorithm
             == "consistent-hashing" : true'
-        - message: spec.hashOnFallback.cookie must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookie)
-            : true'
-        - message: spec.hashOnFallback.cookiePath must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookiePath)
+        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
+            set.
+          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
             : true'
         - message: spec.healthchecks.passive.healthy.interval must not be set.
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
@@ -2850,10 +2848,6 @@ spec:
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
             && has(self.spec.healthchecks.passive.unhealthy) ? !has(self.spec.healthchecks.passive.unhealthy.interval)
             : true'
-        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
-            set.
-          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
-            : true'
         - message: When spec.stickySessions is set, spec.hashOn.input must be set
             to 'none' (no other hash_on fields allowed).
           rule: 'has(self.spec.stickySessions) ? (has(self.spec.hashOn) && has(self.spec.hashOn.input)
diff --git a/test/e2e/manifests/all-in-one-postgres.yaml b/test/e2e/manifests/all-in-one-postgres.yaml
index 826289363..f075473c8 100644
--- a/test/e2e/manifests/all-in-one-postgres.yaml
+++ b/test/e2e/manifests/all-in-one-postgres.yaml
@@ -2242,7 +2242,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2278,7 +2278,7 @@ spec:
                     description: |-
                       Input allows using one of the predefined inputs (ip, consumer, path, none).
                       Set this to `none` if you want to use sticky sessions.
-                      For other parametrized inputs, use one of the fields below.
+                      For other parameterized inputs, use one of the fields below.
                     enum:
                     - ip
                     - consumer
@@ -2836,11 +2836,9 @@ spec:
             is set.
           rule: 'has(self.spec.hashOnFallback) ? has(self.spec.algorithm) && self.spec.algorithm
             == "consistent-hashing" : true'
-        - message: spec.hashOnFallback.cookie must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookie)
-            : true'
-        - message: spec.hashOnFallback.cookiePath must not be set.
-          rule: 'has(self.spec.hashOnFallback) ? !has(self.spec.hashOnFallback.cookiePath)
+        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
+            set.
+          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
             : true'
         - message: spec.healthchecks.passive.healthy.interval must not be set.
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
@@ -2850,10 +2848,6 @@ spec:
           rule: 'has(self.spec.healthchecks) && has(self.spec.healthchecks.passive)
             && has(self.spec.healthchecks.passive.unhealthy) ? !has(self.spec.healthchecks.passive.unhealthy.interval)
             : true'
-        - message: spec.hashOnFallback must not be set when spec.hashOn.cookie is
-            set.
-          rule: 'has(self.spec.hashOn) && has(self.spec.hashOn.cookie) ? !has(self.spec.hashOnFallback)
-            : true'
         - message: When spec.stickySessions is set, spec.hashOn.input must be set
             to 'none' (no other hash_on fields allowed).
           rule: 'has(self.spec.stickySessions) ? (has(self.spec.hashOn) && has(self.spec.hashOn.input)
diff --git a/test/envtest/crds_envtest_test.go b/test/envtest/crds_envtest_test.go
index 073a98084..7b2312054 100644
--- a/test/envtest/crds_envtest_test.go
+++ b/test/envtest/crds_envtest_test.go
@@ -193,7 +193,8 @@ func TestCRDValidations(t *testing.T) {
 				for i, invalidHashOn := range generateInvalidHashOns() {
 					t.Run(fmt.Sprintf("invalidHashOn[%d]", i), func(t *testing.T) {
 						err := createKongUpstreamPolicy(ctx, ctrlClient, ns, configurationv1beta1.KongUpstreamPolicySpec{
-							HashOn: &invalidHashOn,
+							HashOn:    &invalidHashOn,
+							Algorithm: lo.ToPtr("consistent-hashing"),
 						})
 						require.ErrorContains(t, err, "Only one of spec.hashOn.(input|cookie|header|uriCapture|queryArg) can be set.")
 					})
@@ -255,25 +256,85 @@ func TestCRDValidations(t *testing.T) {
 			},
 		},
 		{
-			name: "KongUpstreamPolicy - spec.hashOnFallback.cookie must not be set",
+			name: "KongUpstreamPolicy - spec.hashOnFallback.cookie can be set when spec.hashOn uses header",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				err := createKongUpstreamPolicy(ctx, ctrlClient, ns, configurationv1beta1.KongUpstreamPolicySpec{
+					Algorithm: lo.ToPtr("consistent-hashing"),
+					HashOn: &configurationv1beta1.KongUpstreamHash{
+						Header: lo.ToPtr("header-name"),
+					},
+					HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+						Cookie:     lo.ToPtr("cookie-name"),
+						CookiePath: lo.ToPtr("/"),
+					},
+				})
+				require.NoError(t, err)
+			},
+		},
+		{
+			name: "KongUpstreamPolicy - spec.hashOnFallback.cookie can be set when spec.hashOn uses queryArg",
 			scenario: func(ctx context.Context, t *testing.T, ns string) {
 				err := createKongUpstreamPolicy(ctx, ctrlClient, ns, configurationv1beta1.KongUpstreamPolicySpec{
+					Algorithm: lo.ToPtr("consistent-hashing"),
+					HashOn: &configurationv1beta1.KongUpstreamHash{
+						QueryArg: lo.ToPtr("query-arg-name"),
+					},
 					HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+						Cookie:     lo.ToPtr("cookie-name"),
 						CookiePath: lo.ToPtr("/"),
 					},
 				})
-				require.ErrorContains(t, err, "spec.hashOnFallback.cookiePath must not be set.")
+				require.NoError(t, err)
 			},
 		},
 		{
-			name: "KongUpstreamPolicy - spec.hashOnFallback.cookiePath must not be set",
+			name: "KongUpstreamPolicy - spec.hashOnFallback.cookie can be set when spec.hashOn uses uriCapture",
 			scenario: func(ctx context.Context, t *testing.T, ns string) {
 				err := createKongUpstreamPolicy(ctx, ctrlClient, ns, configurationv1beta1.KongUpstreamPolicySpec{
+					Algorithm: lo.ToPtr("consistent-hashing"),
+					HashOn: &configurationv1beta1.KongUpstreamHash{
+						URICapture: lo.ToPtr("uri-capture-name"),
+					},
 					HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+						Cookie:     lo.ToPtr("cookie-name"),
 						CookiePath: lo.ToPtr("/"),
 					},
 				})
-				require.ErrorContains(t, err, "spec.hashOnFallback.cookiePath must not be set.")
+				require.NoError(t, err)
+			},
+		},
+		{
+			// Ref: https://developer.konghq.com/gateway/entities/upstream/#consistent-hashing
+			// > The hash_fallback setting is invalid and can’t be used if cookie is the primary hashing mechanism.
+			name: "KongUpstreamPolicy - spec.hashOnFallback.cookie can't be set when spec.hashOn uses cookie",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				err := createKongUpstreamPolicy(ctx, ctrlClient, ns, configurationv1beta1.KongUpstreamPolicySpec{
+					Algorithm: lo.ToPtr("consistent-hashing"),
+					HashOn: &configurationv1beta1.KongUpstreamHash{
+						Cookie:     lo.ToPtr("cookie-name"),
+						CookiePath: lo.ToPtr("/cookie"),
+					},
+					HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+						Cookie:     lo.ToPtr("cookie-name-2"),
+						CookiePath: lo.ToPtr("/cookie-2"),
+					},
+				})
+				require.ErrorContains(t, err, "spec.hashOnFallback must not be set when spec.hashOn.cookie is set.")
+			},
+		},
+		{
+			name: "KongUpstreamPolicy - spec.hashOn.uri_capture can be used with spec.hashOnFallback.header",
+			scenario: func(ctx context.Context, t *testing.T, ns string) {
+				err := createKongUpstreamPolicy(ctx, ctrlClient, ns, configurationv1beta1.KongUpstreamPolicySpec{
+					Algorithm: lo.ToPtr("consistent-hashing"),
+					HashOn: &configurationv1beta1.KongUpstreamHash{
+						URICapture: lo.ToPtr("uri-capture-name"),
+					},
+					HashOnFallback: &configurationv1beta1.KongUpstreamHash{
+						Header: lo.ToPtr("header-name"),
+					},
+				})
+				require.NoError(t, err)
 			},
 		},
 		{