Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/v1alpha1/http_listener_policy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ type HTTPListenerPolicySpec struct {
// +optional
// +kubebuilder:validation:MinLength=1
DefaultHostForHttp10 *string `json:"defaultHostForHttp10,omitempty"`

// EarlyRequestHeaderModifier defines header modifications to be applied early in the request processing,
// before route selection.
// For example, if you use ExternalAuthz to add a header, you may want to remove it here, to make
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation mentions "ExternalAuthz" but the correct term appears to be "ExternalAuth" based on the comment. Consider using consistent terminology.

Suggested change
// For example, if you use ExternalAuthz to add a header, you may want to remove it here, to make
// For example, if you use ExternalAuth to add a header, you may want to remove it here, to make

Copilot uses AI. Check for mistakes.
// sure it did not come from the client.
Comment on lines +130 to +131
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// For example, if you use ExternalAuthz to add a header, you may want to remove it here, to make
// sure it did not come from the client.
// For example, if your external auth service adds a header, you should sanitize that header by removing it here,
// to make sure it did not come from the client.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really just trying to get 'sanitize' somewhere in here.

// +optional
EarlyRequestHeaderModifier *gwv1.HTTPHeaderFilter `json:"earlyRequestHeaderModifier,omitempty"`
}

// AccessLog represents the top-level access log configuration.
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,137 @@ spec:
See here for more information: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#config-core-v3-http1protocoloptions
minLength: 1
type: string
earlyRequestHeaderModifier:
description: |-
EarlyRequestHeaderModifier defines header modifications to be applied early in the request processing,
before route selection.
For example, if you use ExternalAuthz to add a header, you may want to remove it here, to make
sure it did not come from the client.
properties:
add:
description: |-
Add adds the given header(s) (name, value) to the request
before the action. It appends to any existing values associated
with the header name.

Input:
GET /foo HTTP/1.1
my-header: foo

Config:
add:
- name: "my-header"
value: "bar,baz"

Output:
GET /foo HTTP/1.1
my-header: foo,bar,baz
items:
description: HTTPHeader represents an HTTP Header name and value
as defined by RFC 7230.
properties:
name:
description: |-
Name is the name of the HTTP Header to be matched. Name matching MUST be
case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).

If multiple entries specify equivalent header names, the first entry with
an equivalent name MUST be considered for a match. Subsequent entries
with an equivalent header name MUST be ignored. Due to the
case-insensitivity of header names, "foo" and "Foo" are considered
equivalent.
maxLength: 256
minLength: 1
pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
type: string
value:
description: Value is the value of HTTP Header to be matched.
maxLength: 4096
minLength: 1
type: string
required:
- name
- value
type: object
maxItems: 16
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
remove:
description: |-
Remove the given header(s) from the HTTP request before the action. The
value of Remove is a list of HTTP header names. Note that the header
names are case-insensitive (see
https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).

Input:
GET /foo HTTP/1.1
my-header1: foo
my-header2: bar
my-header3: baz

Config:
remove: ["my-header1", "my-header3"]

Output:
GET /foo HTTP/1.1
my-header2: bar
items:
type: string
maxItems: 16
type: array
x-kubernetes-list-type: set
set:
description: |-
Set overwrites the request with the given header (name, value)
before the action.

Input:
GET /foo HTTP/1.1
my-header: foo

Config:
set:
- name: "my-header"
value: "bar"

Output:
GET /foo HTTP/1.1
my-header: bar
items:
description: HTTPHeader represents an HTTP Header name and value
as defined by RFC 7230.
properties:
name:
description: |-
Name is the name of the HTTP Header to be matched. Name matching MUST be
case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).

If multiple entries specify equivalent header names, the first entry with
an equivalent name MUST be considered for a match. Subsequent entries
with an equivalent header name MUST be ignored. Due to the
case-insensitivity of header names, "foo" and "Foo" are considered
equivalent.
maxLength: 256
minLength: 1
pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
type: string
value:
description: Value is the value of HTTP Header to be matched.
maxLength: 4096
minLength: 1
type: string
required:
- name
- value
type: object
maxItems: 16
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
type: object
healthCheck:
description: HealthCheck configures [Envoy health checks](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/health_check/v3/health_check.proto)
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
envoytracev3 "github.com/envoyproxy/go-control-plane/envoy/config/trace/v3"
healthcheckv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/health_check/v3"
envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_header_mutationv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/early_header_mutation/header_mutation/v3"
preserve_case_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3"
envoymatcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"google.golang.org/protobuf/proto"
Expand All @@ -22,8 +23,10 @@ import (
"istio.io/istio/pkg/kube/krt"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/ptr"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"

"github.com/kgateway-dev/kgateway/v2/api/v1alpha1"
"github.com/kgateway-dev/kgateway/v2/internal/kgateway/extensions2/pluginutils"
"github.com/kgateway-dev/kgateway/v2/internal/kgateway/utils"
"github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown"
"github.com/kgateway-dev/kgateway/v2/pkg/krtcollections"
Expand Down Expand Up @@ -61,10 +64,11 @@ type httpListenerPolicy struct {
// Since the gateway name can only be determined during translation, the tracing config is split into the provider
// and the actual config. During translation, the default serviceName is set if not already provided
// and the final config is then marshalled.
tracingProvider *envoytracev3.OpenTelemetryConfig
tracingConfig *envoy_hcm.HttpConnectionManager_Tracing
acceptHttp10 *bool
defaultHostForHttp10 *string
tracingProvider *envoytracev3.OpenTelemetryConfig
tracingConfig *envoy_hcm.HttpConnectionManager_Tracing
acceptHttp10 *bool
defaultHostForHttp10 *string
earlyHeaderMutationExtensions []*envoycorev3.TypedExtensionConfig
}

func (d *httpListenerPolicy) CreationTime() time.Time {
Expand Down Expand Up @@ -157,6 +161,11 @@ func (d *httpListenerPolicy) Equals(in any) bool {
return false
}

if !slices.EqualFunc(d.earlyHeaderMutationExtensions, d2.earlyHeaderMutationExtensions, func(a, b *envoycorev3.TypedExtensionConfig) bool {
return proto.Equal(a, b)
}) {
return false
}
return true
}

Expand Down Expand Up @@ -234,21 +243,22 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections) sd
ObjectSource: objSrc,
Policy: i,
PolicyIR: &httpListenerPolicy{
ct: i.CreationTimestamp.Time,
accessLogConfig: accessLog,
accessLogPolicies: i.Spec.AccessLog,
tracingProvider: tracingProvider,
tracingConfig: tracingConfig,
upgradeConfigs: upgradeConfigs,
useRemoteAddress: i.Spec.UseRemoteAddress,
xffNumTrustedHops: xffNumTrustedHops,
serverHeaderTransformation: serverHeaderTransformation,
streamIdleTimeout: streamIdleTimeout,
idleTimeout: idleTimeout,
healthCheckPolicy: healthCheckPolicy,
preserveHttp1HeaderCase: i.Spec.PreserveHttp1HeaderCase,
acceptHttp10: i.Spec.AcceptHttp10,
defaultHostForHttp10: i.Spec.DefaultHostForHttp10,
ct: i.CreationTimestamp.Time,
accessLogConfig: accessLog,
accessLogPolicies: i.Spec.AccessLog,
tracingProvider: tracingProvider,
tracingConfig: tracingConfig,
upgradeConfigs: upgradeConfigs,
useRemoteAddress: i.Spec.UseRemoteAddress,
xffNumTrustedHops: xffNumTrustedHops,
serverHeaderTransformation: serverHeaderTransformation,
streamIdleTimeout: streamIdleTimeout,
idleTimeout: idleTimeout,
healthCheckPolicy: healthCheckPolicy,
preserveHttp1HeaderCase: i.Spec.PreserveHttp1HeaderCase,
acceptHttp10: i.Spec.AcceptHttp10,
defaultHostForHttp10: i.Spec.DefaultHostForHttp10,
earlyHeaderMutationExtensions: convertHeaderMutations(i.Spec.EarlyRequestHeaderModifier),
},
TargetRefs: pluginsdkutils.TargetRefsToPolicyRefs(i.Spec.TargetRefs, i.Spec.TargetSelectors),
Errors: errs,
Expand Down Expand Up @@ -347,6 +357,10 @@ func (p *httpListenerPolicyPluginGwPass) ApplyHCM(
if policy.streamIdleTimeout != nil {
out.StreamIdleTimeout = durationpb.New(*policy.streamIdleTimeout)
}
// early request header modifier
if len(policy.earlyHeaderMutationExtensions) != 0 {
out.EarlyHeaderMutationExtensions = append(out.EarlyHeaderMutationExtensions, policy.earlyHeaderMutationExtensions...)
}

// translate idleTimeout
if policy.idleTimeout != nil {
Expand Down Expand Up @@ -476,3 +490,19 @@ func convertHealthCheckPolicy(policy *v1alpha1.HTTPListenerPolicy) *healthcheckv
}
return nil
}

func convertHeaderMutations(spec *gwv1.HTTPHeaderFilter) []*envoycorev3.TypedExtensionConfig {
mutations := pluginutils.ConvertMutations(spec)
if len(mutations) == 0 {
return nil
}

policy := &envoy_header_mutationv3.HeaderMutation{
Mutations: mutations,
}

return []*envoycorev3.TypedExtensionConfig{{
Name: "envoy.http.early_header_mutation.header_mutation",
TypedConfig: utils.MustMessageToAny(policy),
}}
}
16 changes: 16 additions & 0 deletions internal/kgateway/extensions2/plugins/httplistenerpolicy/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func mergePolicies(
mergePreserveHttp1HeaderCase,
mergeAcceptHttp10,
mergeDefaultHostForHttp10,
mergeEarlyHeaderMutation,
}

for _, mergeFunc := range mergeFuncs {
Expand Down Expand Up @@ -227,3 +228,18 @@ func mergeHealthCheckPolicy(
p1.healthCheckPolicy = p2.healthCheckPolicy
mergeOrigins.SetOne("healthCheckPolicy", p2Ref, p2MergeOrigins)
}

func mergeEarlyHeaderMutation(
p1, p2 *httpListenerPolicy,
p2Ref *ir.AttachedPolicyRef,
p2MergeOrigins ir.MergeOrigins,
opts policy.MergeOptions,
mergeOrigins ir.MergeOrigins,
) {
if !policy.IsMergeable(p1.earlyHeaderMutationExtensions, p2.earlyHeaderMutationExtensions, opts) {
return
}

p1.earlyHeaderMutationExtensions = slices.Clone(p2.earlyHeaderMutationExtensions)
mergeOrigins.SetOne("earlyHeaderMutationExtensions", p2Ref, p2MergeOrigins)
}
Loading