From cda0a14db28d528e903872c7182a2d233064af6e Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Fri, 29 May 2026 15:31:02 +0200 Subject: [PATCH 01/10] feat(api): add host normalization to ClientTrafficPolicy Add a new `host` section to `ClientTrafficPolicy` to normalize the Host/Authority header on the listener, parallel to the existing `path` section: - `host.stripTrailingHostDot` maps to Envoy `strip_trailing_host_dot`, so requests with `Host: example.com.` match routes for `example.com` (NGINX-parity behavior). - `host.stripPortMode` (`Any`/`Matching`) maps to Envoy `strip_any_host_port` / `strip_matching_host_port`. Both were previously only achievable via fragile EnvoyPatchPolicy. This change is split out of #8825 per maintainer review, which scoped that PR to `maxRequestHeaderBytes`. Fixes #8832 Co-authored-by: albsga4 Signed-off-by: Salim Boulkour --- api/v1alpha1/clienttrafficpolicy_types.go | 4 + api/v1alpha1/hostsettings_types.go | 42 ++++++ api/v1alpha1/zz_generated.deepcopy.go | 30 ++++ ...y.envoyproxy.io_clienttrafficpolicies.yaml | 26 ++++ ...y.envoyproxy.io_clienttrafficpolicies.yaml | 26 ++++ internal/gatewayapi/clienttrafficpolicy.go | 18 +++ .../clienttrafficpolicy-host-empty.in.yaml | 27 ++++ .../clienttrafficpolicy-host-empty.out.yaml | 136 +++++++++++++++++ ...ficpolicy-host-strip-port-matching.in.yaml | 28 ++++ ...icpolicy-host-strip-port-matching.out.yaml | 139 ++++++++++++++++++ ...fficpolicy-host-strip-trailing-dot.in.yaml | 28 ++++ ...ficpolicy-host-strip-trailing-dot.out.yaml | 139 ++++++++++++++++++ internal/ir/xds.go | 23 +++ internal/ir/zz_generated.deepcopy.go | 20 +++ internal/xds/translator/listener.go | 14 ++ .../in/xds-ir/host-normalization.yaml | 53 +++++++ .../xds-ir/host-normalization.clusters.yaml | 69 +++++++++ .../xds-ir/host-normalization.endpoints.yaml | 36 +++++ .../xds-ir/host-normalization.listeners.yaml | 103 +++++++++++++ .../out/xds-ir/host-normalization.routes.yaml | 42 ++++++ site/content/en/latest/api/extension_types.md | 33 +++++ .../traffic/host-header-normalization.md | 139 ++++++++++++++++++ test/helm/gateway-crds-helm/all.out.yaml | 26 ++++ test/helm/gateway-crds-helm/e2e.out.yaml | 26 ++++ .../envoy-gateway-crds.out.yaml | 26 ++++ 25 files changed, 1253 insertions(+) create mode 100644 api/v1alpha1/hostsettings_types.go create mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.in.yaml create mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.out.yaml create mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.in.yaml create mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.out.yaml create mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.in.yaml create mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.out.yaml create mode 100644 internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/host-normalization.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/host-normalization.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/host-normalization.routes.yaml create mode 100644 site/content/en/latest/tasks/traffic/host-header-normalization.md diff --git a/api/v1alpha1/clienttrafficpolicy_types.go b/api/v1alpha1/clienttrafficpolicy_types.go index dd2fff5a3e..ce53d6ef96 100644 --- a/api/v1alpha1/clienttrafficpolicy_types.go +++ b/api/v1alpha1/clienttrafficpolicy_types.go @@ -79,6 +79,10 @@ type ClientTrafficPolicySpec struct { // // +optional Path *PathSettings `json:"path,omitempty"` + // Host enables managing how the Host/Authority header set by clients can be normalized. + // + // +optional + Host *HostSettings `json:"host,omitempty"` // HeaderSettings provides configuration for header management. // // +optional diff --git a/api/v1alpha1/hostsettings_types.go b/api/v1alpha1/hostsettings_types.go new file mode 100644 index 0000000000..3fcdf0f91a --- /dev/null +++ b/api/v1alpha1/hostsettings_types.go @@ -0,0 +1,42 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package v1alpha1 + +// StripPortMode determines how the port is stripped from the Host/Authority header +// before route matching. +// +kubebuilder:validation:Enum=Any;Matching +type StripPortMode string + +const ( + // StripPortModeAny strips the port from the Host/Authority header unconditionally. + StripPortModeAny StripPortMode = "Any" + // StripPortModeMatching strips the port from the Host/Authority header only when it + // matches the listener's port. + StripPortModeMatching StripPortMode = "Matching" +) + +// HostSettings provides settings that manage how the incoming Host/Authority header +// set by clients is normalized. +type HostSettings struct { + // StripPortMode determines how the port is stripped from the Host/Authority header + // before route matching. + // "Any" strips the port unconditionally, "Matching" strips it only when it matches + // the listener's port. + // If not set, no port stripping is performed (Envoy default). + // + // +optional + StripPortMode *StripPortMode `json:"stripPortMode,omitempty"` + // StripTrailingHostDot determines if the trailing dot of the host should be removed + // from the Host/Authority header before any processing of the request. + // This affects the upstream host header as well. Without this option, incoming requests + // with host "example.com." will not match routes with domains set to "example.com". + // When the host includes a port (for example "example.com.:443"), only the trailing dot + // from the host section is stripped, leaving the port as-is ("example.com:443"). + // Defaults to false. + // + // +optional + StripTrailingHostDot *bool `json:"stripTrailingHostDot,omitempty"` +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2195616a06..8c9a6042d8 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1466,6 +1466,11 @@ func (in *ClientTrafficPolicySpec) DeepCopyInto(out *ClientTrafficPolicySpec) { *out = new(PathSettings) (*in).DeepCopyInto(*out) } + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(HostSettings) + (*in).DeepCopyInto(*out) + } if in.Headers != nil { in, out := &in.Headers, &out.Headers *out = new(HeaderSettings) @@ -4951,6 +4956,31 @@ func (in *HealthCheckSettings) DeepCopy() *HealthCheckSettings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostSettings) DeepCopyInto(out *HostSettings) { + *out = *in + if in.StripPortMode != nil { + in, out := &in.StripPortMode, &out.StripPortMode + *out = new(StripPortMode) + **out = **in + } + if in.StripTrailingHostDot != nil { + in, out := &in.StripTrailingHostDot, &out.StripTrailingHostDot + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostSettings. +func (in *HostSettings) DeepCopy() *HostSettings { + if in == nil { + return nil + } + out := new(HostSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPEndpoint) DeepCopyInto(out *IPEndpoint) { *out = *in diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 8dd19d576d..3144037ab6 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -830,6 +830,32 @@ spec: required: - path type: object + host: + description: Host enables managing how the Host/Authority header set + by clients can be normalized. + properties: + stripPortMode: + description: |- + StripPortMode determines how the port is stripped from the Host/Authority header + before route matching. + "Any" strips the port unconditionally, "Matching" strips it only when it matches + the listener's port. + If not set, no port stripping is performed (Envoy default). + enum: + - Any + - Matching + type: string + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 43ddd97c0a..2481b43540 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -829,6 +829,32 @@ spec: required: - path type: object + host: + description: Host enables managing how the Host/Authority header set + by clients can be normalized. + properties: + stripPortMode: + description: |- + StripPortMode determines how the port is stripped from the Host/Authority header + before route matching. + "Any" strips the port unconditionally, "Matching" strips it only when it matches + the listener's port. + If not set, no port stripping is performed (Envoy default). + enum: + - Any + - Matching + type: string + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go index bca57a5a96..2246536e1e 100644 --- a/internal/gatewayapi/clienttrafficpolicy.go +++ b/internal/gatewayapi/clienttrafficpolicy.go @@ -697,6 +697,9 @@ func (t *Translator) translateClientTrafficPolicyForListener( // Translate Path Settings translatePathSettings(policy.Spec.Path, httpIR) + // Translate Host Settings + translateHostSettings(policy.Spec.Host, httpIR) + // Translate HTTP1 Settings if err = translateHTTP1Settings(policy.Spec.HTTP1, connection, httpIR); err != nil { err = perr.WithMessage(err, "HTTP1") @@ -839,6 +842,21 @@ func translatePathSettings(pathSettings *egv1a1.PathSettings, httpIR *ir.HTTPLis } } +func translateHostSettings(hostSettings *egv1a1.HostSettings, httpIR *ir.HTTPListener) { + if hostSettings == nil { + return + } + if hostSettings.StripPortMode == nil && hostSettings.StripTrailingHostDot == nil { + return + } + httpIR.Host = &ir.HostSettings{ + StripTrailingHostDot: ptr.Deref(hostSettings.StripTrailingHostDot, false), + } + if hostSettings.StripPortMode != nil { + httpIR.Host.StripPortMode = ir.StripPortMode(*hostSettings.StripPortMode) + } +} + func buildClientTimeout(clientTimeout *egv1a1.ClientTimeout) (*ir.ClientTimeout, error) { // Return early if not set if clientTimeout == nil { diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.in.yaml new file mode 100644 index 0000000000..ff7cab9296 --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.in.yaml @@ -0,0 +1,27 @@ +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + namespace: envoy-gateway + name: target-gateway-1 + spec: + host: {} + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.out.yaml new file mode 100644 index 0000000000..8a95ffac4c --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.out.yaml @@ -0,0 +1,136 @@ +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + name: target-gateway-1 + namespace: envoy-gateway + spec: + host: {} + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http-1 + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/http-1 + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http-1 + name: envoy-gateway/gateway-1/http-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.in.yaml new file mode 100644 index 0000000000..a31cbf3b08 --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.in.yaml @@ -0,0 +1,28 @@ +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + namespace: envoy-gateway + name: target-gateway-1 + spec: + host: + stripPortMode: Matching + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.out.yaml new file mode 100644 index 0000000000..127ae0ff90 --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.out.yaml @@ -0,0 +1,139 @@ +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + name: target-gateway-1 + namespace: envoy-gateway + spec: + host: + stripPortMode: Matching + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http-1 + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/http-1 + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + host: + stripPortMode: Matching + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http-1 + name: envoy-gateway/gateway-1/http-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.in.yaml new file mode 100644 index 0000000000..14f12dd150 --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.in.yaml @@ -0,0 +1,28 @@ +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + namespace: envoy-gateway + name: target-gateway-1 + spec: + host: + stripTrailingHostDot: true + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.out.yaml new file mode 100644 index 0000000000..5b7fa6621f --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.out.yaml @@ -0,0 +1,139 @@ +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + name: target-gateway-1 + namespace: envoy-gateway + spec: + host: + stripTrailingHostDot: true + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http-1 + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/http-1 + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + host: + stripTrailingHostDot: true + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http-1 + name: envoy-gateway/gateway-1/http-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index d8ff851451..85d170abd1 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -316,6 +316,8 @@ type HTTPListener struct { GeoIPProvider *GeoIPProvider `json:"geoIPProvider,omitempty" yaml:"geoIPProvider,omitempty"` // Path contains settings for path URI manipulations Path PathSettings `json:"path,omitempty"` + // Host contains settings for Host/Authority header normalization + Host *HostSettings `json:"host,omitempty" yaml:"host,omitempty"` // HTTP1 provides HTTP/1 configuration on the listener // +optional HTTP1 *HTTP1Settings `json:"http1,omitempty" yaml:"http1,omitempty"` @@ -593,6 +595,27 @@ type PathSettings struct { EscapedSlashesAction PathEscapedSlashAction `json:"escapedSlashesAction" yaml:"escapedSlashesAction"` } +// StripPortMode determines how the port is stripped from the Host/Authority header. +type StripPortMode string + +const ( + // StripPortModeAny strips the port from the Host/Authority header unconditionally. + StripPortModeAny StripPortMode = "Any" + // StripPortModeMatching strips the port from the Host/Authority header only when it + // matches the listener's port. + StripPortModeMatching StripPortMode = "Matching" +) + +// HostSettings holds configuration for Host/Authority header normalization +// +k8s:deepcopy-gen=true +type HostSettings struct { + // StripPortMode determines how the port is stripped from the Host/Authority header. + // An empty value means no port stripping is performed. + StripPortMode StripPortMode `json:"stripPortMode,omitempty" yaml:"stripPortMode,omitempty"` + // StripTrailingHostDot strips the trailing dot from the Host/Authority header before processing. + StripTrailingHostDot bool `json:"stripTrailingHostDot,omitempty" yaml:"stripTrailingHostDot,omitempty"` +} + // ProxyProtocolSettings holds configuration for proxy protocol // +k8s:deepcopy-gen=true type ProxyProtocolSettings struct { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index f4e3a81b02..ecee140465 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -2327,6 +2327,11 @@ func (in *HTTPListener) DeepCopyInto(out *HTTPListener) { (*in).DeepCopyInto(*out) } out.Path = in.Path + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(HostSettings) + **out = **in + } if in.HTTP1 != nil { in, out := &in.HTTP1, &out.HTTP1 *out = new(HTTP1Settings) @@ -2829,6 +2834,21 @@ func (in *HealthCheckSettings) DeepCopy() *HealthCheckSettings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostSettings) DeepCopyInto(out *HostSettings) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostSettings. +func (in *HostSettings) DeepCopy() *HostSettings { + if in == nil { + return nil + } + out := new(HostSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Infra) DeepCopyInto(out *Infra) { *out = *in diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index a68ce651d8..682e278657 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -401,6 +401,20 @@ func (t *Translator) addHCMToXDSListener( RequestIdExtension: buildRequestIDExtension(irListener.RequestID), } + // Normalize the Host/Authority header if configured. + // StripAnyHostPort uses the strip_port_mode oneof, so it must be assigned outside the + // struct literal to avoid a typed-nil that silently breaks xDS translation. + // StripMatchingHostPort is a standalone bool field (not part of the oneof). + if irListener.Host != nil { + switch irListener.Host.StripPortMode { + case ir.StripPortModeAny: + mgr.StripPortMode = &hcmv3.HttpConnectionManager_StripAnyHostPort{StripAnyHostPort: true} + case ir.StripPortModeMatching: + mgr.StripMatchingHostPort = true + } + mgr.StripTrailingHostDot = irListener.Host.StripTrailingHostDot + } + // Set the :scheme header to match the upstream transport protocol (http/https) if configured. // This ensures the correct scheme is sent to backends using TLS when enabled. if irListener.MatchBackendScheme { diff --git a/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml b/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml new file mode 100644 index 0000000000..cb8eb5cda7 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml @@ -0,0 +1,53 @@ +http: +- name: "first-listener" + address: "::" + port: 8081 + hostnames: + - "*" + routes: + - name: "first-route" + hostname: "*" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.1.1.1" + port: 8081 + name: "first-route-dest/backend/0" + host: + stripPortMode: "Any" + stripTrailingHostDot: true +- name: "second-listener" + address: "::" + port: 8082 + hostnames: + - "*" + routes: + - name: "second-route" + hostname: "*" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "2.2.2.2" + port: 8082 + name: "second-route-dest/backend/0" + host: + stripPortMode: "Matching" +- name: "third-listener" + address: "::" + port: 8083 + hostnames: + - "*" + routes: + - name: "third-route" + hostname: "*" + destination: + name: "third-route-dest" + settings: + - endpoints: + - host: "3.3.3.3" + port: 8083 + name: "third-route-dest/backend/0" + host: + stripTrailingHostDot: true diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.clusters.yaml new file mode 100644 index 0000000000..358f31108a --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.clusters.yaml @@ -0,0 +1,69 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: first-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: second-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: third-route-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: third-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.endpoints.yaml new file mode 100644 index 0000000000..59545ddec3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.endpoints.yaml @@ -0,0 +1,36 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.1.1.1 + portValue: 8081 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: first-route-dest/backend/0 +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 2.2.2.2 + portValue: 8082 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: second-route-dest/backend/0 +- clusterName: third-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 3.3.3.3 + portValue: 8083 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: third-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml new file mode 100644 index 0000000000..8f932e0c60 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml @@ -0,0 +1,103 @@ +- address: + socketAddress: + address: '::' + portValue: 8081 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + normalizePath: true + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-8081 + stripAnyHostPort: true + stripTrailingHostDot: true + useRemoteAddress: true + name: first-listener + maxConnectionsToAcceptPerSocketEvent: 1 + name: first-listener + perConnectionBufferLimitBytes: 32768 +- address: + socketAddress: + address: '::' + portValue: 8082 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + normalizePath: true + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: second-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-8082 + stripMatchingHostPort: true + useRemoteAddress: true + name: second-listener + maxConnectionsToAcceptPerSocketEvent: 1 + name: second-listener + perConnectionBufferLimitBytes: 32768 +- address: + socketAddress: + address: '::' + portValue: 8083 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + normalizePath: true + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: third-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-8083 + stripTrailingHostDot: true + useRemoteAddress: true + name: third-listener + maxConnectionsToAcceptPerSocketEvent: 1 + name: third-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.routes.yaml new file mode 100644 index 0000000000..12a38a14ef --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.routes.yaml @@ -0,0 +1,42 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + prefix: / + name: first-route + route: + cluster: first-route-dest + upgradeConfigs: + - upgradeType: websocket +- ignorePortInHostMatching: true + name: second-listener + virtualHosts: + - domains: + - '*' + name: second-listener/* + routes: + - match: + prefix: / + name: second-route + route: + cluster: second-route-dest + upgradeConfigs: + - upgradeType: websocket +- ignorePortInHostMatching: true + name: third-listener + virtualHosts: + - domains: + - '*' + name: third-listener/* + routes: + - match: + prefix: / + name: third-route + route: + cluster: third-route-dest + upgradeConfigs: + - upgradeType: websocket diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 7d78e634ab..87edf3c67e 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -1001,6 +1001,7 @@ _Appears in:_ | `clientIPDetection` | _[ClientIPDetectionSettings](#clientipdetectionsettings)_ | false | | ClientIPDetectionSettings provides configuration for determining the original client IP address for requests. | | `tls` | _[ClientTLSSettings](#clienttlssettings)_ | false | | TLS settings configure TLS termination settings with the downstream client. | | `path` | _[PathSettings](#pathsettings)_ | false | | Path enables managing how the incoming path set by clients can be normalized. | +| `host` | _[HostSettings](#hostsettings)_ | false | | Host enables managing how the Host/Authority header set by clients can be normalized. | | `headers` | _[HeaderSettings](#headersettings)_ | false | | HeaderSettings provides configuration for header management. | | `timeout` | _[ClientTimeout](#clienttimeout)_ | false | | Timeout settings for the client connections. | | `connection` | _[ClientConnection](#clientconnection)_ | false | | Connection includes client connection settings. | @@ -3394,6 +3395,22 @@ _Appears in:_ | `path` | _string_ | true | | Path specifies the HTTP path to match on for health check requests. | +#### HostSettings + + + +HostSettings provides settings that manage how the incoming Host/Authority header +set by clients is normalized. + +_Appears in:_ +- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `stripPortMode` | _[StripPortMode](#stripportmode)_ | false | | StripPortMode determines how the port is stripped from the Host/Authority header
before route matching.
"Any" strips the port unconditionally, "Matching" strips it only when it matches
the listener's port.
If not set, no port stripping is performed (Envoy default). | +| `stripTrailingHostDot` | _boolean_ | false | | StripTrailingHostDot determines if the trailing dot of the host should be removed
from the Host/Authority header before any processing of the request.
This affects the upstream host header as well. Without this option, incoming requests
with host "example.com." will not match routes with domains set to "example.com".
When the host includes a port (for example "example.com.:443"), only the trailing dot
from the host section is stripped, leaving the port as-is ("example.com:443").
Defaults to false. | + + #### IPEndpoint @@ -6082,6 +6099,22 @@ _Appears in:_ | `RegularExpression` | StringMatchRegularExpression :The input string must match the regular expression
specified in the match value.
The regex string must adhere to the syntax documented in
https://github.com/google/re2/wiki/Syntax.
| +#### StripPortMode + +_Underlying type:_ _string_ + +StripPortMode determines how the port is stripped from the Host/Authority header +before route matching. + +_Appears in:_ +- [HostSettings](#hostsettings) + +| Value | Description | +| ----- | ----------- | +| `Any` | StripPortModeAny strips the port from the Host/Authority header unconditionally.
| +| `Matching` | StripPortModeMatching strips the port from the Host/Authority header only when it
matches the listener's port.
| + + #### SubjectAltNames diff --git a/site/content/en/latest/tasks/traffic/host-header-normalization.md b/site/content/en/latest/tasks/traffic/host-header-normalization.md new file mode 100644 index 0000000000..66fd145b5a --- /dev/null +++ b/site/content/en/latest/tasks/traffic/host-header-normalization.md @@ -0,0 +1,139 @@ +--- +title: "Host Normalization" +--- + +This task explains how to configure host normalization settings +using the [ClientTrafficPolicy][] API. + +## Prerequisites + +{{< boilerplate prerequisites >}} + +## Stripping Port from the Host Header + +Some clients include the port in the `Host` (or `:authority`) header +(e.g. `example.com:443`). Use `host.stripPortMode` to have Envoy strip the port +before route matching. + +- `Any` strips the port unconditionally. +- `Matching` strips only when the port matches the listener port. + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the policy is accepted: + +```shell +kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default +``` + +``` +NAME STATUS AGE +strip-host-port Accepted 5s +``` + +## Stripping Trailing Dot from the Host Header + +Envoy does not strip trailing dots from the `Host` header by default, unlike some +other proxies (e.g. NGINX). This means requests with `Host: example.com.` will not +match routes with domains set to `example.com`. Use `host.stripTrailingHostDot` to +normalize these requests. + +When the host includes a port (e.g. `example.com.:443`), only the trailing dot from +the host section is stripped, leaving the port as-is (`example.com:443`). + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + +Verify the policy is accepted: + +```shell +kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default +``` + +``` +NAME STATUS AGE +strip-trailing-dot Accepted 5s +``` + +[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index f0fcbd76a5..532ec54779 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -29147,6 +29147,32 @@ spec: required: - path type: object + host: + description: Host enables managing how the Host/Authority header set + by clients can be normalized. + properties: + stripPortMode: + description: |- + StripPortMode determines how the port is stripped from the Host/Authority header + before route matching. + "Any" strips the port unconditionally, "Matching" strips it only when it matches + the listener's port. + If not set, no port stripping is performed (Envoy default). + enum: + - Any + - Matching + type: string + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index a91c9b9ca8..6cc408f38c 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -5085,6 +5085,32 @@ spec: required: - path type: object + host: + description: Host enables managing how the Host/Authority header set + by clients can be normalized. + properties: + stripPortMode: + description: |- + StripPortMode determines how the port is stripped from the Host/Authority header + before route matching. + "Any" strips the port unconditionally, "Matching" strips it only when it matches + the listener's port. + If not set, no port stripping is performed (Envoy default). + enum: + - Any + - Matching + type: string + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index f926f93ec0..47cbc7a7e0 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -5085,6 +5085,32 @@ spec: required: - path type: object + host: + description: Host enables managing how the Host/Authority header set + by clients can be normalized. + properties: + stripPortMode: + description: |- + StripPortMode determines how the port is stripped from the Host/Authority header + before route matching. + "Any" strips the port unconditionally, "Matching" strips it only when it matches + the listener's port. + If not set, no port stripping is performed (Envoy default). + enum: + - Any + - Matching + type: string + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: From 38ee3100a55caccdeba28f5145771572941c76bb Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Mon, 22 Jun 2026 12:20:50 +0200 Subject: [PATCH 02/10] api: replace host.stripPortMode enum with host.stripPort bool Drop the stripPortMode (Any|Matching) enum on ClientTrafficPolicy's host section and replace it with a stripPort *bool that maps to Envoy's strip_any_host_port (unconditional stripping). The Matching mode mapped to strip_matching_host_port, which compares the Host header port against the Envoy listener port. Since a Gateway listener on e.g. port 80 is translated to an Envoy listener on port 10080, Matching never strips the port clients actually use, so it was silently ineffective. As discussed in the PR review, only unconditional stripping is exposed, expressed as a boolean per maintainer suggestion. Regenerated CRDs, deepcopy, helm snapshots, docs and golden testdata, and renamed the strip-port-matching gatewayapi test to strip-port. The release note is now a fragment under release-notes/current/new_features/ following the split-release-notes workflow (#9312). Signed-off-by: Salim Boulkour --- api/v1alpha1/hostsettings_types.go | 29 ++--- api/v1alpha1/zz_generated.deepcopy.go | 6 +- ...y.envoyproxy.io_clienttrafficpolicies.yaml | 21 ++-- ...y.envoyproxy.io_clienttrafficpolicies.yaml | 21 ++-- internal/gatewayapi/clienttrafficpolicy.go | 6 +- ...ficpolicy-host-strip-port-matching.in.yaml | 28 ----- ...lienttrafficpolicy-host-strip-port.in.yaml | 28 +++++ ...enttrafficpolicy-host-strip-port.out.yaml} | 4 +- internal/ir/xds.go | 17 +-- internal/xds/translator/listener.go | 6 +- .../in/xds-ir/host-normalization.yaml | 104 +++++++++--------- .../xds-ir/host-normalization.listeners.yaml | 2 +- ...-host-normalization-clienttrafficpolicy.md | 1 + site/content/en/latest/api/extension_types.md | 18 +-- .../traffic/host-header-normalization.md | 11 +- test/helm/gateway-crds-helm/all.out.yaml | 21 ++-- test/helm/gateway-crds-helm/e2e.out.yaml | 21 ++-- .../envoy-gateway-crds.out.yaml | 21 ++-- 18 files changed, 169 insertions(+), 196 deletions(-) delete mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.in.yaml create mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml rename internal/gatewayapi/testdata/{clienttrafficpolicy-host-strip-port-matching.out.yaml => clienttrafficpolicy-host-strip-port.out.yaml} (98%) create mode 100644 release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md diff --git a/api/v1alpha1/hostsettings_types.go b/api/v1alpha1/hostsettings_types.go index 3fcdf0f91a..52875b18e3 100644 --- a/api/v1alpha1/hostsettings_types.go +++ b/api/v1alpha1/hostsettings_types.go @@ -5,30 +5,23 @@ package v1alpha1 -// StripPortMode determines how the port is stripped from the Host/Authority header -// before route matching. -// +kubebuilder:validation:Enum=Any;Matching -type StripPortMode string - -const ( - // StripPortModeAny strips the port from the Host/Authority header unconditionally. - StripPortModeAny StripPortMode = "Any" - // StripPortModeMatching strips the port from the Host/Authority header only when it - // matches the listener's port. - StripPortModeMatching StripPortMode = "Matching" -) - // HostSettings provides settings that manage how the incoming Host/Authority header // set by clients is normalized. type HostSettings struct { - // StripPortMode determines how the port is stripped from the Host/Authority header - // before route matching. - // "Any" strips the port unconditionally, "Matching" strips it only when it matches - // the listener's port. + // StripPort determines whether the port is removed from the Host/Authority header + // before route matching. It maps to Envoy's strip_any_host_port, which strips the + // port unconditionally. // If not set, no port stripping is performed (Envoy default). // + // Stripping only the port that matches the listener port (Envoy's + // strip_matching_host_port) is intentionally not offered: Envoy compares against the + // Envoy listener port, which differs from the user-facing Gateway listener port (for + // example, a Gateway listener on port 80 is translated to an Envoy listener on port + // 10080). Matching-port stripping would therefore be silently ineffective for the + // port clients actually use, so only unconditional stripping is exposed. + // // +optional - StripPortMode *StripPortMode `json:"stripPortMode,omitempty"` + StripPort *bool `json:"stripPort,omitempty"` // StripTrailingHostDot determines if the trailing dot of the host should be removed // from the Host/Authority header before any processing of the request. // This affects the upstream host header as well. Without this option, incoming requests diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8c9a6042d8..e271ef2a72 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -4959,9 +4959,9 @@ func (in *HealthCheckSettings) DeepCopy() *HealthCheckSettings { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HostSettings) DeepCopyInto(out *HostSettings) { *out = *in - if in.StripPortMode != nil { - in, out := &in.StripPortMode, &out.StripPortMode - *out = new(StripPortMode) + if in.StripPort != nil { + in, out := &in.StripPort, &out.StripPort + *out = new(bool) **out = **in } if in.StripTrailingHostDot != nil { diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 3144037ab6..39cb734fb8 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -834,17 +834,20 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPortMode: + stripPort: description: |- - StripPortMode determines how the port is stripped from the Host/Authority header - before route matching. - "Any" strips the port unconditionally, "Matching" strips it only when it matches - the listener's port. + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. If not set, no port stripping is performed (Envoy default). - enum: - - Any - - Matching - type: string + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 2481b43540..e8811be28c 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -833,17 +833,20 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPortMode: + stripPort: description: |- - StripPortMode determines how the port is stripped from the Host/Authority header - before route matching. - "Any" strips the port unconditionally, "Matching" strips it only when it matches - the listener's port. + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. If not set, no port stripping is performed (Envoy default). - enum: - - Any - - Matching - type: string + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go index 2246536e1e..d54417cca2 100644 --- a/internal/gatewayapi/clienttrafficpolicy.go +++ b/internal/gatewayapi/clienttrafficpolicy.go @@ -846,15 +846,13 @@ func translateHostSettings(hostSettings *egv1a1.HostSettings, httpIR *ir.HTTPLis if hostSettings == nil { return } - if hostSettings.StripPortMode == nil && hostSettings.StripTrailingHostDot == nil { + if hostSettings.StripPort == nil && hostSettings.StripTrailingHostDot == nil { return } httpIR.Host = &ir.HostSettings{ + StripPort: ptr.Deref(hostSettings.StripPort, false), StripTrailingHostDot: ptr.Deref(hostSettings.StripTrailingHostDot, false), } - if hostSettings.StripPortMode != nil { - httpIR.Host.StripPortMode = ir.StripPortMode(*hostSettings.StripPortMode) - } } func buildClientTimeout(clientTimeout *egv1a1.ClientTimeout) (*ir.ClientTimeout, error) { diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.in.yaml deleted file mode 100644 index a31cbf3b08..0000000000 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.in.yaml +++ /dev/null @@ -1,28 +0,0 @@ -clientTrafficPolicies: -- apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: ClientTrafficPolicy - metadata: - namespace: envoy-gateway - name: target-gateway-1 - spec: - host: - stripPortMode: Matching - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 -gateways: -- apiVersion: gateway.networking.k8s.io/v1 - kind: Gateway - metadata: - namespace: envoy-gateway - name: gateway-1 - spec: - gatewayClassName: envoy-gateway-class - listeners: - - name: http-1 - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: Same diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml new file mode 100644 index 0000000000..982c2b57db --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml @@ -0,0 +1,28 @@ +clientTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + namespace: envoy-gateway + name: target-gateway-1 + spec: + host: + stripPort: true + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml similarity index 98% rename from internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.out.yaml rename to internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml index 127ae0ff90..0d156125e1 100644 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port-matching.out.yaml +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml @@ -6,7 +6,7 @@ clientTrafficPolicies: namespace: envoy-gateway spec: host: - stripPortMode: Matching + stripPort: true targetRef: group: gateway.networking.k8s.io kind: Gateway @@ -119,7 +119,7 @@ xdsIR: - address: 0.0.0.0 externalPort: 80 host: - stripPortMode: Matching + stripPort: true hostnames: - '*' metadata: diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 85d170abd1..b96a592ed9 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -595,23 +595,12 @@ type PathSettings struct { EscapedSlashesAction PathEscapedSlashAction `json:"escapedSlashesAction" yaml:"escapedSlashesAction"` } -// StripPortMode determines how the port is stripped from the Host/Authority header. -type StripPortMode string - -const ( - // StripPortModeAny strips the port from the Host/Authority header unconditionally. - StripPortModeAny StripPortMode = "Any" - // StripPortModeMatching strips the port from the Host/Authority header only when it - // matches the listener's port. - StripPortModeMatching StripPortMode = "Matching" -) - // HostSettings holds configuration for Host/Authority header normalization // +k8s:deepcopy-gen=true type HostSettings struct { - // StripPortMode determines how the port is stripped from the Host/Authority header. - // An empty value means no port stripping is performed. - StripPortMode StripPortMode `json:"stripPortMode,omitempty" yaml:"stripPortMode,omitempty"` + // StripPort strips the port from the Host/Authority header unconditionally + // (Envoy's strip_any_host_port). A false value means no port stripping is performed. + StripPort bool `json:"stripPort,omitempty" yaml:"stripPort,omitempty"` // StripTrailingHostDot strips the trailing dot from the Host/Authority header before processing. StripTrailingHostDot bool `json:"stripTrailingHostDot,omitempty" yaml:"stripTrailingHostDot,omitempty"` } diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index 682e278657..b975b952e1 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -404,13 +404,9 @@ func (t *Translator) addHCMToXDSListener( // Normalize the Host/Authority header if configured. // StripAnyHostPort uses the strip_port_mode oneof, so it must be assigned outside the // struct literal to avoid a typed-nil that silently breaks xDS translation. - // StripMatchingHostPort is a standalone bool field (not part of the oneof). if irListener.Host != nil { - switch irListener.Host.StripPortMode { - case ir.StripPortModeAny: + if irListener.Host.StripPort { mgr.StripPortMode = &hcmv3.HttpConnectionManager_StripAnyHostPort{StripAnyHostPort: true} - case ir.StripPortModeMatching: - mgr.StripMatchingHostPort = true } mgr.StripTrailingHostDot = irListener.Host.StripTrailingHostDot } diff --git a/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml b/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml index cb8eb5cda7..35bfbcb8e8 100644 --- a/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml @@ -1,53 +1,53 @@ http: -- name: "first-listener" - address: "::" - port: 8081 - hostnames: - - "*" - routes: - - name: "first-route" - hostname: "*" - destination: - name: "first-route-dest" - settings: - - endpoints: - - host: "1.1.1.1" - port: 8081 - name: "first-route-dest/backend/0" - host: - stripPortMode: "Any" - stripTrailingHostDot: true -- name: "second-listener" - address: "::" - port: 8082 - hostnames: - - "*" - routes: - - name: "second-route" - hostname: "*" - destination: - name: "second-route-dest" - settings: - - endpoints: - - host: "2.2.2.2" - port: 8082 - name: "second-route-dest/backend/0" - host: - stripPortMode: "Matching" -- name: "third-listener" - address: "::" - port: 8083 - hostnames: - - "*" - routes: - - name: "third-route" - hostname: "*" - destination: - name: "third-route-dest" - settings: - - endpoints: - - host: "3.3.3.3" - port: 8083 - name: "third-route-dest/backend/0" - host: - stripTrailingHostDot: true + - name: "first-listener" + address: "::" + port: 8081 + hostnames: + - "*" + routes: + - name: "first-route" + hostname: "*" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.1.1.1" + port: 8081 + name: "first-route-dest/backend/0" + host: + stripPort: true + stripTrailingHostDot: true + - name: "second-listener" + address: "::" + port: 8082 + hostnames: + - "*" + routes: + - name: "second-route" + hostname: "*" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "2.2.2.2" + port: 8082 + name: "second-route-dest/backend/0" + host: + stripPort: true + - name: "third-listener" + address: "::" + port: 8083 + hostnames: + - "*" + routes: + - name: "third-route" + hostname: "*" + destination: + name: "third-route-dest" + settings: + - endpoints: + - host: "3.3.3.3" + port: 8083 + name: "third-route-dest/backend/0" + host: + stripTrailingHostDot: true diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml index 8f932e0c60..748ed42440 100644 --- a/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml @@ -61,7 +61,7 @@ routeConfigName: second-listener serverHeaderTransformation: PASS_THROUGH statPrefix: http-8082 - stripMatchingHostPort: true + stripAnyHostPort: true useRemoteAddress: true name: second-listener maxConnectionsToAcceptPerSocketEvent: 1 diff --git a/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md b/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md new file mode 100644 index 0000000000..ffbd3220b2 --- /dev/null +++ b/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md @@ -0,0 +1 @@ +Added a new `host` section to `ClientTrafficPolicy` with `stripPort` and `stripTrailingHostDot` fields to normalize the Host/Authority header (port stripping and trailing dot removal) without an EnvoyPatchPolicy. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 87edf3c67e..2925cade08 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3407,7 +3407,7 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `stripPortMode` | _[StripPortMode](#stripportmode)_ | false | | StripPortMode determines how the port is stripped from the Host/Authority header
before route matching.
"Any" strips the port unconditionally, "Matching" strips it only when it matches
the listener's port.
If not set, no port stripping is performed (Envoy default). | +| `stripPort` | _boolean_ | false | | StripPort determines whether the port is removed from the Host/Authority header
before route matching. It maps to Envoy's strip_any_host_port, which strips the
port unconditionally.
If not set, no port stripping is performed (Envoy default).
Stripping only the port that matches the listener port (Envoy's
strip_matching_host_port) is intentionally not offered: Envoy compares against the
Envoy listener port, which differs from the user-facing Gateway listener port (for
example, a Gateway listener on port 80 is translated to an Envoy listener on port
10080). Matching-port stripping would therefore be silently ineffective for the
port clients actually use, so only unconditional stripping is exposed. | | `stripTrailingHostDot` | _boolean_ | false | | StripTrailingHostDot determines if the trailing dot of the host should be removed
from the Host/Authority header before any processing of the request.
This affects the upstream host header as well. Without this option, incoming requests
with host "example.com." will not match routes with domains set to "example.com".
When the host includes a port (for example "example.com.:443"), only the trailing dot
from the host section is stripped, leaving the port as-is ("example.com:443").
Defaults to false. | @@ -6099,22 +6099,6 @@ _Appears in:_ | `RegularExpression` | StringMatchRegularExpression :The input string must match the regular expression
specified in the match value.
The regex string must adhere to the syntax documented in
https://github.com/google/re2/wiki/Syntax.
| -#### StripPortMode - -_Underlying type:_ _string_ - -StripPortMode determines how the port is stripped from the Host/Authority header -before route matching. - -_Appears in:_ -- [HostSettings](#hostsettings) - -| Value | Description | -| ----- | ----------- | -| `Any` | StripPortModeAny strips the port from the Host/Authority header unconditionally.
| -| `Matching` | StripPortModeMatching strips the port from the Host/Authority header only when it
matches the listener's port.
| - - #### SubjectAltNames diff --git a/site/content/en/latest/tasks/traffic/host-header-normalization.md b/site/content/en/latest/tasks/traffic/host-header-normalization.md index 66fd145b5a..b93bf4efd6 100644 --- a/site/content/en/latest/tasks/traffic/host-header-normalization.md +++ b/site/content/en/latest/tasks/traffic/host-header-normalization.md @@ -12,11 +12,8 @@ using the [ClientTrafficPolicy][] API. ## Stripping Port from the Host Header Some clients include the port in the `Host` (or `:authority`) header -(e.g. `example.com:443`). Use `host.stripPortMode` to have Envoy strip the port -before route matching. - -- `Any` strips the port unconditionally. -- `Matching` strips only when the port matches the listener port. +(e.g. `example.com:443`). Set `host.stripPort` to `true` to have Envoy strip the +port unconditionally before route matching. {{< tabpane text=true >}} {{% tab header="Apply from stdin" %}} @@ -34,7 +31,7 @@ spec: kind: Gateway name: eg host: - stripPortMode: Any + stripPort: true EOF ``` @@ -55,7 +52,7 @@ spec: kind: Gateway name: eg host: - stripPortMode: Any + stripPort: true ``` {{% /tab %}} diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 532ec54779..ccc02fbb3b 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -29151,17 +29151,20 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPortMode: + stripPort: description: |- - StripPortMode determines how the port is stripped from the Host/Authority header - before route matching. - "Any" strips the port unconditionally, "Matching" strips it only when it matches - the listener's port. + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. If not set, no port stripping is performed (Envoy default). - enum: - - Any - - Matching - type: string + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 6cc408f38c..c3f8df5eb9 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -5089,17 +5089,20 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPortMode: + stripPort: description: |- - StripPortMode determines how the port is stripped from the Host/Authority header - before route matching. - "Any" strips the port unconditionally, "Matching" strips it only when it matches - the listener's port. + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. If not set, no port stripping is performed (Envoy default). - enum: - - Any - - Matching - type: string + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 47cbc7a7e0..e984391dee 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -5089,17 +5089,20 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPortMode: + stripPort: description: |- - StripPortMode determines how the port is stripped from the Host/Authority header - before route matching. - "Any" strips the port unconditionally, "Matching" strips it only when it matches - the listener's port. + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. If not set, no port stripping is performed (Envoy default). - enum: - - Any - - Matching - type: string + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed From b5c1c19d5ff4362798a4ae02d67a770aaf07dbcf Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Tue, 7 Jul 2026 15:33:32 +0200 Subject: [PATCH 03/10] api: move host normalization settings under headers in ClientTrafficPolicy Per maintainer review on #9115, relocate the host normalization settings (stripPort, stripTrailingHostDot) from the top-level spec.host into spec.headers.host, since the Host/Authority header is a header concern and belongs alongside the other HeaderSettings. The IR representation (HTTPListener.Host) is unchanged; only the CTP API surface and the CTP->IR translation source path move. As a result, setting host normalization now makes spec.headers non-nil, so the listener header defaults (withUnderscoresAction: RejectRequest, already Envoy's default) appear in the IR for the host-only fixtures. Regenerated deepcopy, CRDs, helm snapshots, API docs and golden testdata, and updated the host-header-normalization task doc and release note. Signed-off-by: Salim Boulkour --- api/v1alpha1/clienttrafficpolicy_types.go | 9 +-- api/v1alpha1/zz_generated.deepcopy.go | 10 ++-- ...y.envoyproxy.io_clienttrafficpolicies.yaml | 58 +++++++++---------- ...y.envoyproxy.io_clienttrafficpolicies.yaml | 58 +++++++++---------- internal/gatewayapi/clienttrafficpolicy.go | 4 +- .../clienttrafficpolicy-host-empty.in.yaml | 3 +- .../clienttrafficpolicy-host-empty.out.yaml | 5 +- ...lienttrafficpolicy-host-strip-port.in.yaml | 5 +- ...ienttrafficpolicy-host-strip-port.out.yaml | 7 ++- ...fficpolicy-host-strip-trailing-dot.in.yaml | 5 +- ...ficpolicy-host-strip-trailing-dot.out.yaml | 7 ++- ...-host-normalization-clienttrafficpolicy.md | 2 +- site/content/en/latest/api/extension_types.md | 4 +- .../traffic/host-header-normalization.md | 24 ++++---- test/helm/gateway-crds-helm/all.out.yaml | 58 +++++++++---------- test/helm/gateway-crds-helm/e2e.out.yaml | 58 +++++++++---------- .../envoy-gateway-crds.out.yaml | 58 +++++++++---------- 17 files changed, 197 insertions(+), 178 deletions(-) diff --git a/api/v1alpha1/clienttrafficpolicy_types.go b/api/v1alpha1/clienttrafficpolicy_types.go index ce53d6ef96..202a6954a8 100644 --- a/api/v1alpha1/clienttrafficpolicy_types.go +++ b/api/v1alpha1/clienttrafficpolicy_types.go @@ -79,10 +79,6 @@ type ClientTrafficPolicySpec struct { // // +optional Path *PathSettings `json:"path,omitempty"` - // Host enables managing how the Host/Authority header set by clients can be normalized. - // - // +optional - Host *HostSettings `json:"host,omitempty"` // HeaderSettings provides configuration for header management. // // +optional @@ -187,6 +183,11 @@ type HeaderSettings struct { // // +optional LateResponseHeaders *HTTPHeaderFilter `json:"lateResponseHeaders,omitempty"` + + // Host enables managing how the Host/Authority header set by clients can be normalized. + // + // +optional + Host *HostSettings `json:"host,omitempty"` } // WithUnderscoresAction configures the action to take when an HTTP header with underscores diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e271ef2a72..0098ff45ca 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1466,11 +1466,6 @@ func (in *ClientTrafficPolicySpec) DeepCopyInto(out *ClientTrafficPolicySpec) { *out = new(PathSettings) (*in).DeepCopyInto(*out) } - if in.Host != nil { - in, out := &in.Host, &out.Host - *out = new(HostSettings) - (*in).DeepCopyInto(*out) - } if in.Headers != nil { in, out := &in.Headers, &out.Headers *out = new(HeaderSettings) @@ -4884,6 +4879,11 @@ func (in *HeaderSettings) DeepCopyInto(out *HeaderSettings) { *out = new(HTTPHeaderFilter) (*in).DeepCopyInto(*out) } + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(HostSettings) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderSettings. diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 39cb734fb8..49c6d19fb2 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -499,6 +499,35 @@ spec: EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests and responses. type: boolean + host: + description: Host enables managing how the Host/Authority header + set by clients can be normalized. + properties: + stripPort: + description: |- + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. + If not set, no port stripping is performed (Envoy default). + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object lateResponseHeaders: description: LateResponseHeaders defines settings for global response header modification. @@ -830,35 +859,6 @@ spec: required: - path type: object - host: - description: Host enables managing how the Host/Authority header set - by clients can be normalized. - properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean - stripTrailingHostDot: - description: |- - StripTrailingHostDot determines if the trailing dot of the host should be removed - from the Host/Authority header before any processing of the request. - This affects the upstream host header as well. Without this option, incoming requests - with host "example.com." will not match routes with domains set to "example.com". - When the host includes a port (for example "example.com.:443"), only the trailing dot - from the host section is stripped, leaving the port as-is ("example.com:443"). - Defaults to false. - type: boolean - type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index e8811be28c..4676cbf5de 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -498,6 +498,35 @@ spec: EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests and responses. type: boolean + host: + description: Host enables managing how the Host/Authority header + set by clients can be normalized. + properties: + stripPort: + description: |- + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. + If not set, no port stripping is performed (Envoy default). + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object lateResponseHeaders: description: LateResponseHeaders defines settings for global response header modification. @@ -829,35 +858,6 @@ spec: required: - path type: object - host: - description: Host enables managing how the Host/Authority header set - by clients can be normalized. - properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean - stripTrailingHostDot: - description: |- - StripTrailingHostDot determines if the trailing dot of the host should be removed - from the Host/Authority header before any processing of the request. - This affects the upstream host header as well. Without this option, incoming requests - with host "example.com." will not match routes with domains set to "example.com". - When the host includes a port (for example "example.com.:443"), only the trailing dot - from the host section is stripped, leaving the port as-is ("example.com:443"). - Defaults to false. - type: boolean - type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go index d54417cca2..48d6b71c9d 100644 --- a/internal/gatewayapi/clienttrafficpolicy.go +++ b/internal/gatewayapi/clienttrafficpolicy.go @@ -698,7 +698,9 @@ func (t *Translator) translateClientTrafficPolicyForListener( translatePathSettings(policy.Spec.Path, httpIR) // Translate Host Settings - translateHostSettings(policy.Spec.Host, httpIR) + if policy.Spec.Headers != nil { + translateHostSettings(policy.Spec.Headers.Host, httpIR) + } // Translate HTTP1 Settings if err = translateHTTP1Settings(policy.Spec.HTTP1, connection, httpIR); err != nil { diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.in.yaml index ff7cab9296..45d03da803 100644 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.in.yaml +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.in.yaml @@ -5,7 +5,8 @@ clientTrafficPolicies: namespace: envoy-gateway name: target-gateway-1 spec: - host: {} + headers: + host: {} targetRef: group: gateway.networking.k8s.io kind: Gateway diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.out.yaml index 8a95ffac4c..ca66ed6ca7 100644 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.out.yaml +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-empty.out.yaml @@ -5,7 +5,8 @@ clientTrafficPolicies: name: target-gateway-1 namespace: envoy-gateway spec: - host: {} + headers: + host: {} targetRef: group: gateway.networking.k8s.io kind: Gateway @@ -117,6 +118,8 @@ xdsIR: http: - address: 0.0.0.0 externalPort: 80 + headers: + withUnderscoresAction: RejectRequest hostnames: - '*' metadata: diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml index 982c2b57db..cfd64f954e 100644 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml @@ -5,8 +5,9 @@ clientTrafficPolicies: namespace: envoy-gateway name: target-gateway-1 spec: - host: - stripPort: true + headers: + host: + stripPort: true targetRef: group: gateway.networking.k8s.io kind: Gateway diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml index 0d156125e1..82b1b2c312 100644 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml @@ -5,8 +5,9 @@ clientTrafficPolicies: name: target-gateway-1 namespace: envoy-gateway spec: - host: - stripPort: true + headers: + host: + stripPort: true targetRef: group: gateway.networking.k8s.io kind: Gateway @@ -118,6 +119,8 @@ xdsIR: http: - address: 0.0.0.0 externalPort: 80 + headers: + withUnderscoresAction: RejectRequest host: stripPort: true hostnames: diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.in.yaml index 14f12dd150..e8200f0b55 100644 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.in.yaml +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.in.yaml @@ -5,8 +5,9 @@ clientTrafficPolicies: namespace: envoy-gateway name: target-gateway-1 spec: - host: - stripTrailingHostDot: true + headers: + host: + stripTrailingHostDot: true targetRef: group: gateway.networking.k8s.io kind: Gateway diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.out.yaml index 5b7fa6621f..c21a2f7f8b 100644 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.out.yaml +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-trailing-dot.out.yaml @@ -5,8 +5,9 @@ clientTrafficPolicies: name: target-gateway-1 namespace: envoy-gateway spec: - host: - stripTrailingHostDot: true + headers: + host: + stripTrailingHostDot: true targetRef: group: gateway.networking.k8s.io kind: Gateway @@ -118,6 +119,8 @@ xdsIR: http: - address: 0.0.0.0 externalPort: 80 + headers: + withUnderscoresAction: RejectRequest host: stripTrailingHostDot: true hostnames: diff --git a/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md b/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md index ffbd3220b2..d58f316e94 100644 --- a/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md +++ b/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md @@ -1 +1 @@ -Added a new `host` section to `ClientTrafficPolicy` with `stripPort` and `stripTrailingHostDot` fields to normalize the Host/Authority header (port stripping and trailing dot removal) without an EnvoyPatchPolicy. +Added a new `host` section under `ClientTrafficPolicy`'s `headers` with `stripPort` and `stripTrailingHostDot` fields to normalize the Host/Authority header (port stripping and trailing dot removal) without an EnvoyPatchPolicy. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 2925cade08..6a170bfbd1 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -1001,7 +1001,6 @@ _Appears in:_ | `clientIPDetection` | _[ClientIPDetectionSettings](#clientipdetectionsettings)_ | false | | ClientIPDetectionSettings provides configuration for determining the original client IP address for requests. | | `tls` | _[ClientTLSSettings](#clienttlssettings)_ | false | | TLS settings configure TLS termination settings with the downstream client. | | `path` | _[PathSettings](#pathsettings)_ | false | | Path enables managing how the incoming path set by clients can be normalized. | -| `host` | _[HostSettings](#hostsettings)_ | false | | Host enables managing how the Host/Authority header set by clients can be normalized. | | `headers` | _[HeaderSettings](#headersettings)_ | false | | HeaderSettings provides configuration for header management. | | `timeout` | _[ClientTimeout](#clienttimeout)_ | false | | Timeout settings for the client connections. | | `connection` | _[ClientConnection](#clientconnection)_ | false | | Connection includes client connection settings. | @@ -3347,6 +3346,7 @@ _Appears in:_ | `requestID` | _[RequestIDAction](#requestidaction)_ | false | | RequestID configures Envoy's behavior for handling the `X-Request-ID` header.
When omitted default behavior is `Generate` which builds the `X-Request-ID` for every request
and ignores pre-existing values from the edge.
(An "edge request" refers to a request from an external client to the Envoy entrypoint.) | | `earlyRequestHeaders` | _[HTTPHeaderFilter](#httpheaderfilter)_ | false | | EarlyRequestHeaders defines settings for early request header modification, before envoy performs
routing, tracing and built-in header manipulation. | | `lateResponseHeaders` | _[HTTPHeaderFilter](#httpheaderfilter)_ | false | | LateResponseHeaders defines settings for global response header modification. | +| `host` | _[HostSettings](#hostsettings)_ | false | | Host enables managing how the Host/Authority header set by clients can be normalized. | #### HealthCheck @@ -3403,7 +3403,7 @@ HostSettings provides settings that manage how the incoming Host/Authority heade set by clients is normalized. _Appears in:_ -- [ClientTrafficPolicySpec](#clienttrafficpolicyspec) +- [HeaderSettings](#headersettings) | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | diff --git a/site/content/en/latest/tasks/traffic/host-header-normalization.md b/site/content/en/latest/tasks/traffic/host-header-normalization.md index b93bf4efd6..0952fb9e2f 100644 --- a/site/content/en/latest/tasks/traffic/host-header-normalization.md +++ b/site/content/en/latest/tasks/traffic/host-header-normalization.md @@ -12,7 +12,7 @@ using the [ClientTrafficPolicy][] API. ## Stripping Port from the Host Header Some clients include the port in the `Host` (or `:authority`) header -(e.g. `example.com:443`). Set `host.stripPort` to `true` to have Envoy strip the +(e.g. `example.com:443`). Set `headers.host.stripPort` to `true` to have Envoy strip the port unconditionally before route matching. {{< tabpane text=true >}} @@ -30,8 +30,9 @@ spec: - group: gateway.networking.k8s.io kind: Gateway name: eg - host: - stripPort: true + headers: + host: + stripPort: true EOF ``` @@ -51,8 +52,9 @@ spec: - group: gateway.networking.k8s.io kind: Gateway name: eg - host: - stripPort: true + headers: + host: + stripPort: true ``` {{% /tab %}} @@ -73,7 +75,7 @@ strip-host-port Accepted 5s Envoy does not strip trailing dots from the `Host` header by default, unlike some other proxies (e.g. NGINX). This means requests with `Host: example.com.` will not -match routes with domains set to `example.com`. Use `host.stripTrailingHostDot` to +match routes with domains set to `example.com`. Use `headers.host.stripTrailingHostDot` to normalize these requests. When the host includes a port (e.g. `example.com.:443`), only the trailing dot from @@ -94,8 +96,9 @@ spec: - group: gateway.networking.k8s.io kind: Gateway name: eg - host: - stripTrailingHostDot: true + headers: + host: + stripTrailingHostDot: true EOF ``` @@ -115,8 +118,9 @@ spec: - group: gateway.networking.k8s.io kind: Gateway name: eg - host: - stripTrailingHostDot: true + headers: + host: + stripTrailingHostDot: true ``` {{% /tab %}} diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index ccc02fbb3b..2a260688e1 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -28816,6 +28816,35 @@ spec: EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests and responses. type: boolean + host: + description: Host enables managing how the Host/Authority header + set by clients can be normalized. + properties: + stripPort: + description: |- + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. + If not set, no port stripping is performed (Envoy default). + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object lateResponseHeaders: description: LateResponseHeaders defines settings for global response header modification. @@ -29147,35 +29176,6 @@ spec: required: - path type: object - host: - description: Host enables managing how the Host/Authority header set - by clients can be normalized. - properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean - stripTrailingHostDot: - description: |- - StripTrailingHostDot determines if the trailing dot of the host should be removed - from the Host/Authority header before any processing of the request. - This affects the upstream host header as well. Without this option, incoming requests - with host "example.com." will not match routes with domains set to "example.com". - When the host includes a port (for example "example.com.:443"), only the trailing dot - from the host section is stripped, leaving the port as-is ("example.com:443"). - Defaults to false. - type: boolean - type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index c3f8df5eb9..ab1355b98c 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -4754,6 +4754,35 @@ spec: EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests and responses. type: boolean + host: + description: Host enables managing how the Host/Authority header + set by clients can be normalized. + properties: + stripPort: + description: |- + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. + If not set, no port stripping is performed (Envoy default). + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object lateResponseHeaders: description: LateResponseHeaders defines settings for global response header modification. @@ -5085,35 +5114,6 @@ spec: required: - path type: object - host: - description: Host enables managing how the Host/Authority header set - by clients can be normalized. - properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean - stripTrailingHostDot: - description: |- - StripTrailingHostDot determines if the trailing dot of the host should be removed - from the Host/Authority header before any processing of the request. - This affects the upstream host header as well. Without this option, incoming requests - with host "example.com." will not match routes with domains set to "example.com". - When the host includes a port (for example "example.com.:443"), only the trailing dot - from the host section is stripped, leaving the port as-is ("example.com:443"). - Defaults to false. - type: boolean - type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index e984391dee..4accbd4591 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -4754,6 +4754,35 @@ spec: EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests and responses. type: boolean + host: + description: Host enables managing how the Host/Authority header + set by clients can be normalized. + properties: + stripPort: + description: |- + StripPort determines whether the port is removed from the Host/Authority header + before route matching. It maps to Envoy's strip_any_host_port, which strips the + port unconditionally. + If not set, no port stripping is performed (Envoy default). + + Stripping only the port that matches the listener port (Envoy's + strip_matching_host_port) is intentionally not offered: Envoy compares against the + Envoy listener port, which differs from the user-facing Gateway listener port (for + example, a Gateway listener on port 80 is translated to an Envoy listener on port + 10080). Matching-port stripping would therefore be silently ineffective for the + port clients actually use, so only unconditional stripping is exposed. + type: boolean + stripTrailingHostDot: + description: |- + StripTrailingHostDot determines if the trailing dot of the host should be removed + from the Host/Authority header before any processing of the request. + This affects the upstream host header as well. Without this option, incoming requests + with host "example.com." will not match routes with domains set to "example.com". + When the host includes a port (for example "example.com.:443"), only the trailing dot + from the host section is stripped, leaving the port as-is ("example.com:443"). + Defaults to false. + type: boolean + type: object lateResponseHeaders: description: LateResponseHeaders defines settings for global response header modification. @@ -5085,35 +5114,6 @@ spec: required: - path type: object - host: - description: Host enables managing how the Host/Authority header set - by clients can be normalized. - properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean - stripTrailingHostDot: - description: |- - StripTrailingHostDot determines if the trailing dot of the host should be removed - from the Host/Authority header before any processing of the request. - This affects the upstream host header as well. Without this option, incoming requests - with host "example.com." will not match routes with domains set to "example.com". - When the host includes a port (for example "example.com.:443"), only the trailing dot - from the host section is stripped, leaving the port as-is ("example.com:443"). - Defaults to false. - type: boolean - type: object http1: description: HTTP1 provides HTTP/1 configuration on the listener. properties: From 7a0156daf1625473cf46fa6f7112eea2bb74e0eb Mon Sep 17 00:00:00 2001 From: Salim B <9945903+sboulkour@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:00:53 +0200 Subject: [PATCH 04/10] Update docs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Salim B <9945903+sboulkour@users.noreply.github.com> --- .../en/latest/tasks/traffic/host-header-normalization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/en/latest/tasks/traffic/host-header-normalization.md b/site/content/en/latest/tasks/traffic/host-header-normalization.md index 0952fb9e2f..3f7e0ec3a3 100644 --- a/site/content/en/latest/tasks/traffic/host-header-normalization.md +++ b/site/content/en/latest/tasks/traffic/host-header-normalization.md @@ -13,7 +13,7 @@ using the [ClientTrafficPolicy][] API. Some clients include the port in the `Host` (or `:authority`) header (e.g. `example.com:443`). Set `headers.host.stripPort` to `true` to have Envoy strip the -port unconditionally before route matching. +port unconditionally from the Host/`:authority` header (this affects the upstream host header as well) before route matching. {{< tabpane text=true >}} {{% tab header="Apply from stdin" %}} From 3e3aad3c2601eed9249c15233bfd078f639da7a1 Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Tue, 7 Jul 2026 17:10:43 +0200 Subject: [PATCH 05/10] api: clarify StripPort normalizes the forwarded Host header Address Copilot review on #9115: Envoy's strip_any_host_port not only strips the port for route matching but also normalizes the actual Host/:authority value forwarded upstream. Make the StripPort godoc state this explicitly (mirroring StripTrailingHostDot's upstream-header note), so the generated CRD/API docs reflect the real effect. Regenerated CRDs, helm snapshots and API docs. Signed-off-by: Salim Boulkour --- api/v1alpha1/hostsettings_types.go | 8 +++++--- .../gateway.envoyproxy.io_clienttrafficpolicies.yaml | 8 +++++--- .../gateway.envoyproxy.io_clienttrafficpolicies.yaml | 8 +++++--- site/content/en/latest/api/extension_types.md | 2 +- test/helm/gateway-crds-helm/all.out.yaml | 8 +++++--- test/helm/gateway-crds-helm/e2e.out.yaml | 8 +++++--- test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml | 8 +++++--- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/api/v1alpha1/hostsettings_types.go b/api/v1alpha1/hostsettings_types.go index 52875b18e3..a0e06881d3 100644 --- a/api/v1alpha1/hostsettings_types.go +++ b/api/v1alpha1/hostsettings_types.go @@ -8,9 +8,11 @@ package v1alpha1 // HostSettings provides settings that manage how the incoming Host/Authority header // set by clients is normalized. type HostSettings struct { - // StripPort determines whether the port is removed from the Host/Authority header - // before route matching. It maps to Envoy's strip_any_host_port, which strips the - // port unconditionally. + // StripPort determines whether the port is removed from the Host/Authority header. + // It maps to Envoy's strip_any_host_port, which strips the port unconditionally. + // The port is removed before route matching, and this affects the upstream host header + // as well: backends and access logs see the normalized Host/:authority value without + // the port. // If not set, no port stripping is performed (Envoy default). // // Stripping only the port that matches the listener port (Envoy's diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 49c6d19fb2..943857ae08 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -505,9 +505,11 @@ spec: properties: stripPort: description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. + StripPort determines whether the port is removed from the Host/Authority header. + It maps to Envoy's strip_any_host_port, which strips the port unconditionally. + The port is removed before route matching, and this affects the upstream host header + as well: backends and access logs see the normalized Host/:authority value without + the port. If not set, no port stripping is performed (Envoy default). Stripping only the port that matches the listener port (Envoy's diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 4676cbf5de..317237ebf0 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -504,9 +504,11 @@ spec: properties: stripPort: description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. + StripPort determines whether the port is removed from the Host/Authority header. + It maps to Envoy's strip_any_host_port, which strips the port unconditionally. + The port is removed before route matching, and this affects the upstream host header + as well: backends and access logs see the normalized Host/:authority value without + the port. If not set, no port stripping is performed (Envoy default). Stripping only the port that matches the listener port (Envoy's diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 6a170bfbd1..d1324cfb7f 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3407,7 +3407,7 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `stripPort` | _boolean_ | false | | StripPort determines whether the port is removed from the Host/Authority header
before route matching. It maps to Envoy's strip_any_host_port, which strips the
port unconditionally.
If not set, no port stripping is performed (Envoy default).
Stripping only the port that matches the listener port (Envoy's
strip_matching_host_port) is intentionally not offered: Envoy compares against the
Envoy listener port, which differs from the user-facing Gateway listener port (for
example, a Gateway listener on port 80 is translated to an Envoy listener on port
10080). Matching-port stripping would therefore be silently ineffective for the
port clients actually use, so only unconditional stripping is exposed. | +| `stripPort` | _boolean_ | false | | StripPort determines whether the port is removed from the Host/Authority header.
It maps to Envoy's strip_any_host_port, which strips the port unconditionally.
The port is removed before route matching, and this affects the upstream host header
as well: backends and access logs see the normalized Host/:authority value without
the port.
If not set, no port stripping is performed (Envoy default).
Stripping only the port that matches the listener port (Envoy's
strip_matching_host_port) is intentionally not offered: Envoy compares against the
Envoy listener port, which differs from the user-facing Gateway listener port (for
example, a Gateway listener on port 80 is translated to an Envoy listener on port
10080). Matching-port stripping would therefore be silently ineffective for the
port clients actually use, so only unconditional stripping is exposed. | | `stripTrailingHostDot` | _boolean_ | false | | StripTrailingHostDot determines if the trailing dot of the host should be removed
from the Host/Authority header before any processing of the request.
This affects the upstream host header as well. Without this option, incoming requests
with host "example.com." will not match routes with domains set to "example.com".
When the host includes a port (for example "example.com.:443"), only the trailing dot
from the host section is stripped, leaving the port as-is ("example.com:443").
Defaults to false. | diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 2a260688e1..98a4d04692 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -28822,9 +28822,11 @@ spec: properties: stripPort: description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. + StripPort determines whether the port is removed from the Host/Authority header. + It maps to Envoy's strip_any_host_port, which strips the port unconditionally. + The port is removed before route matching, and this affects the upstream host header + as well: backends and access logs see the normalized Host/:authority value without + the port. If not set, no port stripping is performed (Envoy default). Stripping only the port that matches the listener port (Envoy's diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index ab1355b98c..f104376c04 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -4760,9 +4760,11 @@ spec: properties: stripPort: description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. + StripPort determines whether the port is removed from the Host/Authority header. + It maps to Envoy's strip_any_host_port, which strips the port unconditionally. + The port is removed before route matching, and this affects the upstream host header + as well: backends and access logs see the normalized Host/:authority value without + the port. If not set, no port stripping is performed (Envoy default). Stripping only the port that matches the listener port (Envoy's diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 4accbd4591..76126f8309 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -4760,9 +4760,11 @@ spec: properties: stripPort: description: |- - StripPort determines whether the port is removed from the Host/Authority header - before route matching. It maps to Envoy's strip_any_host_port, which strips the - port unconditionally. + StripPort determines whether the port is removed from the Host/Authority header. + It maps to Envoy's strip_any_host_port, which strips the port unconditionally. + The port is removed before route matching, and this affects the upstream host header + as well: backends and access logs see the normalized Host/:authority value without + the port. If not set, no port stripping is performed (Envoy default). Stripping only the port that matches the listener port (Envoy's From 5ae24dc9313d63ccdf4eeea5b3ac285420c974d9 Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Wed, 8 Jul 2026 09:56:44 +0200 Subject: [PATCH 06/10] api: drop StripPort from ClientTrafficPolicy host normalization Scope the first iteration of host normalization down to trailing-dot removal only, removing the StripPort field (Envoy's strip_any_host_port) per maintainer feedback. Signed-off-by: Salim Boulkour --- api/v1alpha1/hostsettings_types.go | 16 -- api/v1alpha1/zz_generated.deepcopy.go | 5 - ...y.envoyproxy.io_clienttrafficpolicies.yaml | 16 -- ...y.envoyproxy.io_clienttrafficpolicies.yaml | 16 -- internal/gatewayapi/clienttrafficpolicy.go | 3 +- ...lienttrafficpolicy-host-strip-port.in.yaml | 29 ---- ...ienttrafficpolicy-host-strip-port.out.yaml | 142 ------------------ internal/ir/xds.go | 3 - internal/xds/translator/listener.go | 5 - .../in/xds-ir/host-normalization.yaml | 3 - .../xds-ir/host-normalization.listeners.yaml | 2 - ...-host-normalization-clienttrafficpolicy.md | 2 +- site/content/en/latest/api/extension_types.md | 1 - .../traffic/host-header-normalization.md | 62 -------- test/helm/gateway-crds-helm/all.out.yaml | 16 -- test/helm/gateway-crds-helm/e2e.out.yaml | 16 -- .../envoy-gateway-crds.out.yaml | 16 -- 17 files changed, 2 insertions(+), 351 deletions(-) delete mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml delete mode 100644 internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml diff --git a/api/v1alpha1/hostsettings_types.go b/api/v1alpha1/hostsettings_types.go index a0e06881d3..94c0197ba9 100644 --- a/api/v1alpha1/hostsettings_types.go +++ b/api/v1alpha1/hostsettings_types.go @@ -8,22 +8,6 @@ package v1alpha1 // HostSettings provides settings that manage how the incoming Host/Authority header // set by clients is normalized. type HostSettings struct { - // StripPort determines whether the port is removed from the Host/Authority header. - // It maps to Envoy's strip_any_host_port, which strips the port unconditionally. - // The port is removed before route matching, and this affects the upstream host header - // as well: backends and access logs see the normalized Host/:authority value without - // the port. - // If not set, no port stripping is performed (Envoy default). - // - // Stripping only the port that matches the listener port (Envoy's - // strip_matching_host_port) is intentionally not offered: Envoy compares against the - // Envoy listener port, which differs from the user-facing Gateway listener port (for - // example, a Gateway listener on port 80 is translated to an Envoy listener on port - // 10080). Matching-port stripping would therefore be silently ineffective for the - // port clients actually use, so only unconditional stripping is exposed. - // - // +optional - StripPort *bool `json:"stripPort,omitempty"` // StripTrailingHostDot determines if the trailing dot of the host should be removed // from the Host/Authority header before any processing of the request. // This affects the upstream host header as well. Without this option, incoming requests diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0098ff45ca..bdb9768c6d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -4959,11 +4959,6 @@ func (in *HealthCheckSettings) DeepCopy() *HealthCheckSettings { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HostSettings) DeepCopyInto(out *HostSettings) { *out = *in - if in.StripPort != nil { - in, out := &in.StripPort, &out.StripPort - *out = new(bool) - **out = **in - } if in.StripTrailingHostDot != nil { in, out := &in.StripTrailingHostDot, &out.StripTrailingHostDot *out = new(bool) diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 943857ae08..b873c582aa 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -503,22 +503,6 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header. - It maps to Envoy's strip_any_host_port, which strips the port unconditionally. - The port is removed before route matching, and this affects the upstream host header - as well: backends and access logs see the normalized Host/:authority value without - the port. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index 317237ebf0..5b73c4161a 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -502,22 +502,6 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header. - It maps to Envoy's strip_any_host_port, which strips the port unconditionally. - The port is removed before route matching, and this affects the upstream host header - as well: backends and access logs see the normalized Host/:authority value without - the port. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go index 48d6b71c9d..4ae517884f 100644 --- a/internal/gatewayapi/clienttrafficpolicy.go +++ b/internal/gatewayapi/clienttrafficpolicy.go @@ -848,11 +848,10 @@ func translateHostSettings(hostSettings *egv1a1.HostSettings, httpIR *ir.HTTPLis if hostSettings == nil { return } - if hostSettings.StripPort == nil && hostSettings.StripTrailingHostDot == nil { + if hostSettings.StripTrailingHostDot == nil { return } httpIR.Host = &ir.HostSettings{ - StripPort: ptr.Deref(hostSettings.StripPort, false), StripTrailingHostDot: ptr.Deref(hostSettings.StripTrailingHostDot, false), } } diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml deleted file mode 100644 index cfd64f954e..0000000000 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.in.yaml +++ /dev/null @@ -1,29 +0,0 @@ -clientTrafficPolicies: - - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: ClientTrafficPolicy - metadata: - namespace: envoy-gateway - name: target-gateway-1 - spec: - headers: - host: - stripPort: true - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 -gateways: - - apiVersion: gateway.networking.k8s.io/v1 - kind: Gateway - metadata: - namespace: envoy-gateway - name: gateway-1 - spec: - gatewayClassName: envoy-gateway-class - listeners: - - name: http-1 - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: Same diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml deleted file mode 100644 index 82b1b2c312..0000000000 --- a/internal/gatewayapi/testdata/clienttrafficpolicy-host-strip-port.out.yaml +++ /dev/null @@ -1,142 +0,0 @@ -clientTrafficPolicies: -- apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: ClientTrafficPolicy - metadata: - name: target-gateway-1 - namespace: envoy-gateway - spec: - headers: - host: - stripPort: true - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 - status: - ancestors: - - ancestorRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 - namespace: envoy-gateway - conditions: - - lastTransitionTime: null - message: Policy has been accepted. - reason: Accepted - status: "True" - type: Accepted - - lastTransitionTime: null - message: spec.targetRef is deprecated, use spec.targetRefs instead - reason: DeprecatedField - status: "True" - type: Warning - controllerName: gateway.envoyproxy.io/gatewayclass-controller -gateways: -- apiVersion: gateway.networking.k8s.io/v1 - kind: Gateway - metadata: - name: gateway-1 - namespace: envoy-gateway - spec: - gatewayClassName: envoy-gateway-class - listeners: - - allowedRoutes: - namespaces: - from: Same - name: http-1 - port: 80 - protocol: HTTP - status: - listeners: - - attachedRoutes: 0 - conditions: - - lastTransitionTime: null - message: Sending translated listener configuration to the data plane - reason: Programmed - status: "True" - type: Programmed - - lastTransitionTime: null - message: Listener has been successfully translated - reason: Accepted - status: "True" - type: Accepted - - lastTransitionTime: null - message: Listener references have been resolved - reason: ResolvedRefs - status: "True" - type: ResolvedRefs - name: http-1 - supportedKinds: - - group: gateway.networking.k8s.io - kind: HTTPRoute - - group: gateway.networking.k8s.io - kind: GRPCRoute -infraIR: - envoy-gateway/gateway-1: - proxy: - listeners: - - name: envoy-gateway/gateway-1/http-1 - ports: - - containerPort: 10080 - name: http-80 - protocol: HTTP - servicePort: 80 - metadata: - labels: - gateway.envoyproxy.io/owning-gateway-name: gateway-1 - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway - ownerReference: - kind: GatewayClass - name: envoy-gateway-class - name: envoy-gateway/gateway-1 - namespace: envoy-gateway-system -xdsIR: - envoy-gateway/gateway-1: - accessLog: - json: - - path: /dev/stdout - globalResources: - proxyServiceCluster: - metadata: - kind: Service - name: envoy-envoy-gateway-gateway-1-196ae069 - namespace: envoy-gateway-system - sectionName: "8080" - name: envoy-gateway/gateway-1 - settings: - - addressType: IP - endpoints: - - host: 7.6.5.4 - port: 8080 - zone: zone1 - metadata: - kind: Service - name: envoy-envoy-gateway-gateway-1-196ae069 - namespace: envoy-gateway-system - sectionName: "8080" - name: envoy-gateway/gateway-1 - protocol: TCP - http: - - address: 0.0.0.0 - externalPort: 80 - headers: - withUnderscoresAction: RejectRequest - host: - stripPort: true - hostnames: - - '*' - metadata: - kind: Gateway - name: gateway-1 - namespace: envoy-gateway - sectionName: http-1 - name: envoy-gateway/gateway-1/http-1 - path: - escapedSlashesAction: UnescapeAndRedirect - mergeSlashes: true - port: 10080 - readyListener: - address: 0.0.0.0 - ipFamily: IPv4 - path: /ready - port: 19003 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index b96a592ed9..38269eb5e1 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -598,9 +598,6 @@ type PathSettings struct { // HostSettings holds configuration for Host/Authority header normalization // +k8s:deepcopy-gen=true type HostSettings struct { - // StripPort strips the port from the Host/Authority header unconditionally - // (Envoy's strip_any_host_port). A false value means no port stripping is performed. - StripPort bool `json:"stripPort,omitempty" yaml:"stripPort,omitempty"` // StripTrailingHostDot strips the trailing dot from the Host/Authority header before processing. StripTrailingHostDot bool `json:"stripTrailingHostDot,omitempty" yaml:"stripTrailingHostDot,omitempty"` } diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index b975b952e1..1f8569dfcb 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -402,12 +402,7 @@ func (t *Translator) addHCMToXDSListener( } // Normalize the Host/Authority header if configured. - // StripAnyHostPort uses the strip_port_mode oneof, so it must be assigned outside the - // struct literal to avoid a typed-nil that silently breaks xDS translation. if irListener.Host != nil { - if irListener.Host.StripPort { - mgr.StripPortMode = &hcmv3.HttpConnectionManager_StripAnyHostPort{StripAnyHostPort: true} - } mgr.StripTrailingHostDot = irListener.Host.StripTrailingHostDot } diff --git a/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml b/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml index 35bfbcb8e8..2058a4269a 100644 --- a/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml @@ -15,7 +15,6 @@ http: port: 8081 name: "first-route-dest/backend/0" host: - stripPort: true stripTrailingHostDot: true - name: "second-listener" address: "::" @@ -32,8 +31,6 @@ http: - host: "2.2.2.2" port: 8082 name: "second-route-dest/backend/0" - host: - stripPort: true - name: "third-listener" address: "::" port: 8083 diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml index 748ed42440..9347b89b8e 100644 --- a/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml @@ -26,7 +26,6 @@ routeConfigName: first-listener serverHeaderTransformation: PASS_THROUGH statPrefix: http-8081 - stripAnyHostPort: true stripTrailingHostDot: true useRemoteAddress: true name: first-listener @@ -61,7 +60,6 @@ routeConfigName: second-listener serverHeaderTransformation: PASS_THROUGH statPrefix: http-8082 - stripAnyHostPort: true useRemoteAddress: true name: second-listener maxConnectionsToAcceptPerSocketEvent: 1 diff --git a/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md b/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md index d58f316e94..e05b6bfa59 100644 --- a/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md +++ b/release-notes/current/new_features/9115-host-normalization-clienttrafficpolicy.md @@ -1 +1 @@ -Added a new `host` section under `ClientTrafficPolicy`'s `headers` with `stripPort` and `stripTrailingHostDot` fields to normalize the Host/Authority header (port stripping and trailing dot removal) without an EnvoyPatchPolicy. +Added a new `host` section under `ClientTrafficPolicy`'s `headers` with a `stripTrailingHostDot` field to normalize the Host/Authority header (trailing dot removal) without an EnvoyPatchPolicy. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index d1324cfb7f..ea29423c2e 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -3407,7 +3407,6 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `stripPort` | _boolean_ | false | | StripPort determines whether the port is removed from the Host/Authority header.
It maps to Envoy's strip_any_host_port, which strips the port unconditionally.
The port is removed before route matching, and this affects the upstream host header
as well: backends and access logs see the normalized Host/:authority value without
the port.
If not set, no port stripping is performed (Envoy default).
Stripping only the port that matches the listener port (Envoy's
strip_matching_host_port) is intentionally not offered: Envoy compares against the
Envoy listener port, which differs from the user-facing Gateway listener port (for
example, a Gateway listener on port 80 is translated to an Envoy listener on port
10080). Matching-port stripping would therefore be silently ineffective for the
port clients actually use, so only unconditional stripping is exposed. | | `stripTrailingHostDot` | _boolean_ | false | | StripTrailingHostDot determines if the trailing dot of the host should be removed
from the Host/Authority header before any processing of the request.
This affects the upstream host header as well. Without this option, incoming requests
with host "example.com." will not match routes with domains set to "example.com".
When the host includes a port (for example "example.com.:443"), only the trailing dot
from the host section is stripped, leaving the port as-is ("example.com:443").
Defaults to false. | diff --git a/site/content/en/latest/tasks/traffic/host-header-normalization.md b/site/content/en/latest/tasks/traffic/host-header-normalization.md index 3f7e0ec3a3..5db0b8c967 100644 --- a/site/content/en/latest/tasks/traffic/host-header-normalization.md +++ b/site/content/en/latest/tasks/traffic/host-header-normalization.md @@ -9,68 +9,6 @@ using the [ClientTrafficPolicy][] API. {{< boilerplate prerequisites >}} -## Stripping Port from the Host Header - -Some clients include the port in the `Host` (or `:authority`) header -(e.g. `example.com:443`). Set `headers.host.stripPort` to `true` to have Envoy strip the -port unconditionally from the Host/`:authority` header (this affects the upstream host header as well) before route matching. - -{{< tabpane text=true >}} -{{% tab header="Apply from stdin" %}} - -```shell -cat <}} - -Verify the policy is accepted: - -```shell -kubectl get clienttrafficpolicies.gateway.envoyproxy.io -n default -``` - -``` -NAME STATUS AGE -strip-host-port Accepted 5s -``` - ## Stripping Trailing Dot from the Host Header Envoy does not strip trailing dots from the `Host` header by default, unlike some diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 98a4d04692..55beeb684e 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -28820,22 +28820,6 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header. - It maps to Envoy's strip_any_host_port, which strips the port unconditionally. - The port is removed before route matching, and this affects the upstream host header - as well: backends and access logs see the normalized Host/:authority value without - the port. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index f104376c04..fffd7340e1 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -4758,22 +4758,6 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header. - It maps to Envoy's strip_any_host_port, which strips the port unconditionally. - The port is removed before route matching, and this affects the upstream host header - as well: backends and access logs see the normalized Host/:authority value without - the port. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 76126f8309..1d7dff35e1 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -4758,22 +4758,6 @@ spec: description: Host enables managing how the Host/Authority header set by clients can be normalized. properties: - stripPort: - description: |- - StripPort determines whether the port is removed from the Host/Authority header. - It maps to Envoy's strip_any_host_port, which strips the port unconditionally. - The port is removed before route matching, and this affects the upstream host header - as well: backends and access logs see the normalized Host/:authority value without - the port. - If not set, no port stripping is performed (Envoy default). - - Stripping only the port that matches the listener port (Envoy's - strip_matching_host_port) is intentionally not offered: Envoy compares against the - Envoy listener port, which differs from the user-facing Gateway listener port (for - example, a Gateway listener on port 80 is translated to an Envoy listener on port - 10080). Matching-port stripping would therefore be silently ineffective for the - port clients actually use, so only unconditional stripping is exposed. - type: boolean stripTrailingHostDot: description: |- StripTrailingHostDot determines if the trailing dot of the host should be removed From f82d5e8890f0f9a0d56db61955d7ed59b81db380 Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Wed, 8 Jul 2026 10:02:55 +0200 Subject: [PATCH 07/10] docs, test: tidy host normalization for trailing-dot-only scope - docs: qualify the field reference as spec.headers.host.stripTrailingHostDot (review feedback endorsed by @arkodg) - test: drop the now-redundant third xDS listener; keep one listener enabling stripTrailingHostDot and one control with no host settings Signed-off-by: Salim Boulkour --- .../in/xds-ir/host-normalization.yaml | 17 ---------- .../xds-ir/host-normalization.clusters.yaml | 23 ------------- .../xds-ir/host-normalization.endpoints.yaml | 12 ------- .../xds-ir/host-normalization.listeners.yaml | 34 ------------------- .../out/xds-ir/host-normalization.routes.yaml | 14 -------- .../traffic/host-header-normalization.md | 2 +- 6 files changed, 1 insertion(+), 101 deletions(-) diff --git a/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml b/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml index 2058a4269a..49bb82173f 100644 --- a/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/host-normalization.yaml @@ -31,20 +31,3 @@ http: - host: "2.2.2.2" port: 8082 name: "second-route-dest/backend/0" - - name: "third-listener" - address: "::" - port: 8083 - hostnames: - - "*" - routes: - - name: "third-route" - hostname: "*" - destination: - name: "third-route-dest" - settings: - - endpoints: - - host: "3.3.3.3" - port: 8083 - name: "third-route-dest/backend/0" - host: - stripTrailingHostDot: true diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.clusters.yaml index 358f31108a..2597993e4b 100644 --- a/internal/xds/translator/testdata/out/xds-ir/host-normalization.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.clusters.yaml @@ -44,26 +44,3 @@ name: second-route-dest perConnectionBufferLimitBytes: 32768 type: EDS -- circuitBreakers: - thresholds: - - maxRetries: 1024 - commonLbConfig: {} - connectTimeout: 10s - dnsLookupFamily: V4_PREFERRED - edsClusterConfig: - edsConfig: - ads: {} - resourceApiVersion: V3 - serviceName: third-route-dest - ignoreHealthOnHostRemoval: true - loadBalancingPolicy: - policies: - - typedExtensionConfig: - name: envoy.load_balancing_policies.least_request - typedConfig: - '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest - localityLbConfig: - localityWeightedLbConfig: {} - name: third-route-dest - perConnectionBufferLimitBytes: 32768 - type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.endpoints.yaml index 59545ddec3..9ad09dd91b 100644 --- a/internal/xds/translator/testdata/out/xds-ir/host-normalization.endpoints.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.endpoints.yaml @@ -22,15 +22,3 @@ loadBalancingWeight: 1 locality: region: second-route-dest/backend/0 -- clusterName: third-route-dest - endpoints: - - lbEndpoints: - - endpoint: - address: - socketAddress: - address: 3.3.3.3 - portValue: 8083 - loadBalancingWeight: 1 - loadBalancingWeight: 1 - locality: - region: third-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml index 9347b89b8e..0e7f782798 100644 --- a/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.listeners.yaml @@ -65,37 +65,3 @@ maxConnectionsToAcceptPerSocketEvent: 1 name: second-listener perConnectionBufferLimitBytes: 32768 -- address: - socketAddress: - address: '::' - portValue: 8083 - defaultFilterChain: - filters: - - name: envoy.filters.network.http_connection_manager - typedConfig: - '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - commonHttpProtocolOptions: - headersWithUnderscoresAction: REJECT_REQUEST - http2ProtocolOptions: - initialConnectionWindowSize: 1048576 - initialStreamWindowSize: 65536 - maxConcurrentStreams: 100 - httpFilters: - - name: envoy.filters.http.router - typedConfig: - '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - suppressEnvoyHeaders: true - normalizePath: true - rds: - configSource: - ads: {} - resourceApiVersion: V3 - routeConfigName: third-listener - serverHeaderTransformation: PASS_THROUGH - statPrefix: http-8083 - stripTrailingHostDot: true - useRemoteAddress: true - name: third-listener - maxConnectionsToAcceptPerSocketEvent: 1 - name: third-listener - perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/host-normalization.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/host-normalization.routes.yaml index 12a38a14ef..ff93cfff36 100644 --- a/internal/xds/translator/testdata/out/xds-ir/host-normalization.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/host-normalization.routes.yaml @@ -26,17 +26,3 @@ cluster: second-route-dest upgradeConfigs: - upgradeType: websocket -- ignorePortInHostMatching: true - name: third-listener - virtualHosts: - - domains: - - '*' - name: third-listener/* - routes: - - match: - prefix: / - name: third-route - route: - cluster: third-route-dest - upgradeConfigs: - - upgradeType: websocket diff --git a/site/content/en/latest/tasks/traffic/host-header-normalization.md b/site/content/en/latest/tasks/traffic/host-header-normalization.md index 5db0b8c967..e0a885a524 100644 --- a/site/content/en/latest/tasks/traffic/host-header-normalization.md +++ b/site/content/en/latest/tasks/traffic/host-header-normalization.md @@ -13,7 +13,7 @@ using the [ClientTrafficPolicy][] API. Envoy does not strip trailing dots from the `Host` header by default, unlike some other proxies (e.g. NGINX). This means requests with `Host: example.com.` will not -match routes with domains set to `example.com`. Use `headers.host.stripTrailingHostDot` to +match routes with domains set to `example.com`. Use `spec.headers.host.stripTrailingHostDot` to normalize these requests. When the host includes a port (e.g. `example.com.:443`), only the trailing dot from From 9f826849aa231d83d9d88fcd7e5eb501d5003e6d Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Wed, 15 Jul 2026 15:15:15 +0200 Subject: [PATCH 08/10] refactor: fold host settings translation into translateListenerHeaderSettings Handle headers.host alongside the other HeaderSettings sub-fields instead of through a separate helper and call site. Co-Authored-By: Claude Fable 5 Signed-off-by: Salim Boulkour --- internal/gatewayapi/clienttrafficpolicy.go | 23 ++++++---------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go index 4ae517884f..9a0d488bb0 100644 --- a/internal/gatewayapi/clienttrafficpolicy.go +++ b/internal/gatewayapi/clienttrafficpolicy.go @@ -697,11 +697,6 @@ func (t *Translator) translateClientTrafficPolicyForListener( // Translate Path Settings translatePathSettings(policy.Spec.Path, httpIR) - // Translate Host Settings - if policy.Spec.Headers != nil { - translateHostSettings(policy.Spec.Headers.Host, httpIR) - } - // Translate HTTP1 Settings if err = translateHTTP1Settings(policy.Spec.HTTP1, connection, httpIR); err != nil { err = perr.WithMessage(err, "HTTP1") @@ -844,18 +839,6 @@ func translatePathSettings(pathSettings *egv1a1.PathSettings, httpIR *ir.HTTPLis } } -func translateHostSettings(hostSettings *egv1a1.HostSettings, httpIR *ir.HTTPListener) { - if hostSettings == nil { - return - } - if hostSettings.StripTrailingHostDot == nil { - return - } - httpIR.Host = &ir.HostSettings{ - StripTrailingHostDot: ptr.Deref(hostSettings.StripTrailingHostDot, false), - } -} - func buildClientTimeout(clientTimeout *egv1a1.ClientTimeout) (*ir.ClientTimeout, error) { // Return early if not set if clientTimeout == nil { @@ -942,6 +925,12 @@ func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, http } } + if headerSettings.Host != nil && headerSettings.Host.StripTrailingHostDot != nil { + httpIR.Host = &ir.HostSettings{ + StripTrailingHostDot: ptr.Deref(headerSettings.Host.StripTrailingHostDot, false), + } + } + var errs error if headerSettings.EarlyRequestHeaders != nil { From 9d10b3f7001a0a7a15a6ea350fc4e87bf4177a16 Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Wed, 15 Jul 2026 15:16:24 +0200 Subject: [PATCH 09/10] refactor: drop redundant ptr.Deref on StripTrailingHostDot The field is nil-checked just above, so dereference it directly. Co-Authored-By: Claude Fable 5 Signed-off-by: Salim Boulkour --- internal/gatewayapi/clienttrafficpolicy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go index 9a0d488bb0..588d4ec96e 100644 --- a/internal/gatewayapi/clienttrafficpolicy.go +++ b/internal/gatewayapi/clienttrafficpolicy.go @@ -927,7 +927,7 @@ func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, http if headerSettings.Host != nil && headerSettings.Host.StripTrailingHostDot != nil { httpIR.Host = &ir.HostSettings{ - StripTrailingHostDot: ptr.Deref(headerSettings.Host.StripTrailingHostDot, false), + StripTrailingHostDot: *headerSettings.Host.StripTrailingHostDot, } } From 4bebf559da3c9caa411a14691a5ec34d988cb4e4 Mon Sep 17 00:00:00 2001 From: Salim Boulkour Date: Wed, 15 Jul 2026 15:22:44 +0200 Subject: [PATCH 10/10] test: add e2e test for stripTrailingHostDot Sends a request with a trailing dot in the Host header through a listener with ClientTrafficPolicy headers.host.stripTrailingHostDot enabled, and verifies the route matches and the backend receives the normalized Host header. Co-Authored-By: Claude Fable 5 Signed-off-by: Salim Boulkour --- test/e2e/testdata/host-normalization.yaml | 32 +++++++++ test/e2e/tests/host_normalization.go | 83 +++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 test/e2e/testdata/host-normalization.yaml create mode 100644 test/e2e/tests/host_normalization.go diff --git a/test/e2e/testdata/host-normalization.yaml b/test/e2e/testdata/host-normalization.yaml new file mode 100644 index 0000000000..b88f904641 --- /dev/null +++ b/test/e2e/testdata/host-normalization.yaml @@ -0,0 +1,32 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: ClientTrafficPolicy +metadata: + name: strip-trailing-host-dot-ctp + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace + headers: + host: + stripTrailingHostDot: true +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-host-normalization + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: + - host-normalization.example.com + rules: + - matches: + - path: + type: PathPrefix + value: /host-normalization + backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/test/e2e/tests/host_normalization.go b/test/e2e/tests/host_normalization.go new file mode 100644 index 0000000000..1259be7f00 --- /dev/null +++ b/test/e2e/tests/host_normalization.go @@ -0,0 +1,83 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" +) + +func init() { + ConformanceTests = append(ConformanceTests, HostNormalizationTest) +} + +var HostNormalizationTest = suite.ConformanceTest{ + ShortName: "HostNormalization", + Description: "Strip the trailing dot from the Host header before route matching", + Manifests: []string{"testdata/host-normalization.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-host-normalization", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gwapiv1.HTTPRoute{}, false, routeNN) + + ancestorRef := gwapiv1.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + ClientTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "strip-trailing-host-dot-ctp", Namespace: ns}, suite.ControllerName, ancestorRef) + + t.Run("host without trailing dot should match the route", func(t *testing.T) { + expected := http.ExpectedResponse{ + Request: http.Request{ + Host: "host-normalization.example.com", + Path: "/host-normalization", + }, + Response: http.Response{ + StatusCodes: []int{200}, + }, + Namespace: ns, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expected) + }) + + t.Run("host with trailing dot should be normalized and match the route", func(t *testing.T) { + expected := http.ExpectedResponse{ + Request: http.Request{ + Host: "host-normalization.example.com.", + Path: "/host-normalization", + }, + // The trailing dot is stripped before routing, so the backend + // receives the normalized Host header. + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Host: "host-normalization.example.com", + Path: "/host-normalization", + }, + }, + Response: http.Response{ + StatusCodes: []int{200}, + }, + Namespace: ns, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expected) + }) + }, +}