diff --git a/apis/projectcontour/v1alpha1/contourconfig.go b/apis/projectcontour/v1alpha1/contourconfig.go index b2bbdfaa580..5b6eb1ad8bd 100644 --- a/apis/projectcontour/v1alpha1/contourconfig.go +++ b/apis/projectcontour/v1alpha1/contourconfig.go @@ -336,6 +336,18 @@ type EnvoyListenerConfig struct { // +optional DisableMergeSlashes *bool `json:"disableMergeSlashes,omitempty"` + // Defines the action to be applied to the Server header on the response path. + // When configured as overwrite, overwrites any Server header with "envoy". + // When configured as append_if_absent, if a Server header is present, pass it through, otherwise set it to "envoy". + // When configured as pass_through, pass through the value of the Server header, and do not append a header if none is present. + // + // Values: `overwrite` (default), `append_if_absent`, `pass_through` + // + // Other values will produce an error. + // Contour's default is overwrite. + // +optional + ServerHeaderTransformation ServerHeaderTransformationType `json:"serverHeaderTransformation,omitempty"` + // ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer // See https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/listener.proto#envoy-api-msg-listener-connectionbalanceconfig // for more information. @@ -532,6 +544,21 @@ const ( AllClusterDNSFamily ClusterDNSFamilyType = "all" ) +// ServerHeaderTransformation defines the action to be applied to the Server header on the response path +type ServerHeaderTransformationType string + +const ( + // Overwrite any Server header with "envoy". + // This is the default value. + OverwriteServerHeader ServerHeaderTransformationType = "overwrite" + // If no Server header is present, set it to "envoy". + // If a Server header is present, pass it through. + AppendIfAbsentServerHeader ServerHeaderTransformationType = "append_if_absent" + // Pass through the value of the Server header, and do not append a header + // if none is present. + PassThroughServerHeader ServerHeaderTransformationType = "pass_through" +) + // ClusterParameters holds various configurable cluster values. type ClusterParameters struct { // DNSLookupFamily defines how external names are looked up diff --git a/changelogs/unreleased/4906-Vishal-Chdhry-minor.md b/changelogs/unreleased/4906-Vishal-Chdhry-minor.md new file mode 100644 index 00000000000..28ee2246201 --- /dev/null +++ b/changelogs/unreleased/4906-Vishal-Chdhry-minor.md @@ -0,0 +1,6 @@ +## Enable configuring Server header transformation + +Envoy's treatment of the Server header on responses can now be configured in the Contour config file or ContourConfiguration CRD. +When configured as `overwrite`, Envoy overwrites any Server header with "envoy". +When configured as `append_if_absent`, ⁣if a Server header is present, Envoy will pass it through, otherwise, it will set it to "envoy". +When configured as `pass_through`, Envoy passes through the value of the Server header and does not append a header if none is present. \ No newline at end of file diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index 15ee63a1eb2..e45f673ccfe 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -348,6 +348,7 @@ func (s *Server) doServe() error { DefaultHTTPVersions: parseDefaultHTTPVersions(contourConfiguration.Envoy.DefaultHTTPVersions), AllowChunkedLength: !*contourConfiguration.Envoy.Listener.DisableAllowChunkedLength, MergeSlashes: !*contourConfiguration.Envoy.Listener.DisableMergeSlashes, + ServerHeaderTransformation: contourConfiguration.Envoy.Listener.ServerHeaderTransformation, XffNumTrustedHops: *contourConfiguration.Envoy.Network.XffNumTrustedHops, ConnectionBalancer: contourConfiguration.Envoy.Listener.ConnectionBalancer, } diff --git a/cmd/contour/servecontext.go b/cmd/contour/servecontext.go index 1857575ae64..dab3bcd267e 100644 --- a/cmd/contour/servecontext.go +++ b/cmd/contour/servecontext.go @@ -378,6 +378,16 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha } } + var serverHeaderTransformation contour_api_v1alpha1.ServerHeaderTransformationType + switch ctx.Config.ServerHeaderTransformation { + case config.OverwriteServerHeader: + serverHeaderTransformation = contour_api_v1alpha1.OverwriteServerHeader + case config.AppendIfAbsentServerHeader: + serverHeaderTransformation = contour_api_v1alpha1.AppendIfAbsentServerHeader + case config.PassThroughServerHeader: + serverHeaderTransformation = contour_api_v1alpha1.PassThroughServerHeader + } + policy := &contour_api_v1alpha1.PolicyConfig{ RequestHeadersPolicy: &contour_api_v1alpha1.HeadersPolicy{ Set: ctx.Config.Policy.RequestHeadersPolicy.Set, @@ -439,10 +449,11 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha }, Envoy: &contour_api_v1alpha1.EnvoyConfig{ Listener: &contour_api_v1alpha1.EnvoyListenerConfig{ - UseProxyProto: &ctx.useProxyProto, - DisableAllowChunkedLength: &ctx.Config.DisableAllowChunkedLength, - DisableMergeSlashes: &ctx.Config.DisableMergeSlashes, - ConnectionBalancer: ctx.Config.Listener.ConnectionBalancer, + UseProxyProto: &ctx.useProxyProto, + DisableAllowChunkedLength: &ctx.Config.DisableAllowChunkedLength, + DisableMergeSlashes: &ctx.Config.DisableMergeSlashes, + ServerHeaderTransformation: serverHeaderTransformation, + ConnectionBalancer: ctx.Config.Listener.ConnectionBalancer, TLS: &contour_api_v1alpha1.EnvoyTLS{ MinimumProtocolVersion: ctx.Config.TLS.MinimumProtocolVersion, CipherSuites: cipherSuites, diff --git a/cmd/contour/servecontext_test.go b/cmd/contour/servecontext_test.go index 79fe7d174a7..4d5583caa2b 100644 --- a/cmd/contour/servecontext_test.go +++ b/cmd/contour/servecontext_test.go @@ -406,9 +406,10 @@ func TestConvertServeContext(t *testing.T) { Namespace: "projectcontour", }, Listener: &contour_api_v1alpha1.EnvoyListenerConfig{ - UseProxyProto: ref.To(false), - DisableAllowChunkedLength: ref.To(false), - DisableMergeSlashes: ref.To(false), + UseProxyProto: ref.To(false), + DisableAllowChunkedLength: ref.To(false), + DisableMergeSlashes: ref.To(false), + ServerHeaderTransformation: contour_api_v1alpha1.OverwriteServerHeader, TLS: &contour_api_v1alpha1.EnvoyTLS{ MinimumProtocolVersion: "", }, @@ -688,6 +689,16 @@ func TestConvertServeContext(t *testing.T) { return cfg }, }, + "server header transformation": { + getServeContext: func(ctx *serveContext) *serveContext { + ctx.Config.ServerHeaderTransformation = config.AppendIfAbsentServerHeader + return ctx + }, + getContourConfiguration: func(cfg contour_api_v1alpha1.ContourConfigurationSpec) contour_api_v1alpha1.ContourConfigurationSpec { + cfg.Envoy.Listener.ServerHeaderTransformation = contour_api_v1alpha1.AppendIfAbsentServerHeader + return cfg + }, + }, } for name, tc := range cases { diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index f90d4157af8..dd2efeed57c 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -181,6 +181,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the Server + header on the response path. When configured as overwrite, + overwrites any Server header with \"envoy\". When configured + as append_if_absent, if a Server header is present, pass + it through, otherwise set it to \"envoy\". When configured + as pass_through, pass through the value of the Server header, + and do not append a header if none is present. \n Values: + `overwrite` (default), `append_if_absent`, `pass_through` + \n Other values will produce an error. Contour's default + is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. @@ -3184,6 +3196,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the + Server header on the response path. When configured + as overwrite, overwrites any Server header with \"envoy\". + When configured as append_if_absent, if a Server header + is present, pass it through, otherwise set it to \"envoy\". + When configured as pass_through, pass through the value + of the Server header, and do not append a header if + none is present. \n Values: `overwrite` (default), `append_if_absent`, + `pass_through` \n Other values will produce an error. + Contour's default is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index f1d71f5317d..b78617dd09e 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -394,6 +394,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the Server + header on the response path. When configured as overwrite, + overwrites any Server header with \"envoy\". When configured + as append_if_absent, if a Server header is present, pass + it through, otherwise set it to \"envoy\". When configured + as pass_through, pass through the value of the Server header, + and do not append a header if none is present. \n Values: + `overwrite` (default), `append_if_absent`, `pass_through` + \n Other values will produce an error. Contour's default + is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. @@ -3397,6 +3409,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the + Server header on the response path. When configured + as overwrite, overwrites any Server header with \"envoy\". + When configured as append_if_absent, if a Server header + is present, pass it through, otherwise set it to \"envoy\". + When configured as pass_through, pass through the value + of the Server header, and do not append a header if + none is present. \n Values: `overwrite` (default), `append_if_absent`, + `pass_through` \n Other values will produce an error. + Contour's default is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 7938480df00..156a14c33e3 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -195,6 +195,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the Server + header on the response path. When configured as overwrite, + overwrites any Server header with \"envoy\". When configured + as append_if_absent, if a Server header is present, pass + it through, otherwise set it to \"envoy\". When configured + as pass_through, pass through the value of the Server header, + and do not append a header if none is present. \n Values: + `overwrite` (default), `append_if_absent`, `pass_through` + \n Other values will produce an error. Contour's default + is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. @@ -3198,6 +3210,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the + Server header on the response path. When configured + as overwrite, overwrites any Server header with \"envoy\". + When configured as append_if_absent, if a Server header + is present, pass it through, otherwise set it to \"envoy\". + When configured as pass_through, pass through the value + of the Server header, and do not append a header if + none is present. \n Values: `overwrite` (default), `append_if_absent`, + `pass_through` \n Other values will produce an error. + Contour's default is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index 19386a51050..6a2f369ba3d 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -400,6 +400,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the Server + header on the response path. When configured as overwrite, + overwrites any Server header with \"envoy\". When configured + as append_if_absent, if a Server header is present, pass + it through, otherwise set it to \"envoy\". When configured + as pass_through, pass through the value of the Server header, + and do not append a header if none is present. \n Values: + `overwrite` (default), `append_if_absent`, `pass_through` + \n Other values will produce an error. Contour's default + is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. @@ -3403,6 +3415,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the + Server header on the response path. When configured + as overwrite, overwrites any Server header with \"envoy\". + When configured as append_if_absent, if a Server header + is present, pass it through, otherwise set it to \"envoy\". + When configured as pass_through, pass through the value + of the Server header, and do not append a header if + none is present. \n Values: `overwrite` (default), `append_if_absent`, + `pass_through` \n Other values will produce an error. + Contour's default is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index 72a80895e9b..1fbd097b9de 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -394,6 +394,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the Server + header on the response path. When configured as overwrite, + overwrites any Server header with \"envoy\". When configured + as append_if_absent, if a Server header is present, pass + it through, otherwise set it to \"envoy\". When configured + as pass_through, pass through the value of the Server header, + and do not append a header if none is present. \n Values: + `overwrite` (default), `append_if_absent`, `pass_through` + \n Other values will produce an error. Contour's default + is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. @@ -3397,6 +3409,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + serverHeaderTransformation: + description: "Defines the action to be applied to the + Server header on the response path. When configured + as overwrite, overwrites any Server header with \"envoy\". + When configured as append_if_absent, if a Server header + is present, pass it through, otherwise set it to \"envoy\". + When configured as pass_through, pass through the value + of the Server header, and do not append a header if + none is present. \n Values: `overwrite` (default), `append_if_absent`, + `pass_through` \n Other values will produce an error. + Contour's default is overwrite." + type: string tls: description: TLS holds various configurable Envoy TLS listener values. diff --git a/internal/contourconfig/contourconfiguration.go b/internal/contourconfig/contourconfiguration.go index 3dc93ea3711..edbe699123a 100644 --- a/internal/contourconfig/contourconfiguration.go +++ b/internal/contourconfig/contourconfiguration.go @@ -74,10 +74,11 @@ func Defaults() contour_api_v1alpha1.ContourConfigurationSpec { }, Envoy: &contour_api_v1alpha1.EnvoyConfig{ Listener: &contour_api_v1alpha1.EnvoyListenerConfig{ - UseProxyProto: ref.To(false), - DisableAllowChunkedLength: ref.To(false), - DisableMergeSlashes: ref.To(false), - ConnectionBalancer: "", + UseProxyProto: ref.To(false), + DisableAllowChunkedLength: ref.To(false), + DisableMergeSlashes: ref.To(false), + ServerHeaderTransformation: contour_api_v1alpha1.OverwriteServerHeader, + ConnectionBalancer: "", TLS: &contour_api_v1alpha1.EnvoyTLS{ MinimumProtocolVersion: "1.2", CipherSuites: contour_api_v1alpha1.DefaultTLSCiphers, diff --git a/internal/contourconfig/contourconfiguration_test.go b/internal/contourconfig/contourconfiguration_test.go index 33237721611..408ad4abace 100644 --- a/internal/contourconfig/contourconfiguration_test.go +++ b/internal/contourconfig/contourconfiguration_test.go @@ -52,10 +52,11 @@ func TestOverlayOnDefaults(t *testing.T) { }, Envoy: &contour_api_v1alpha1.EnvoyConfig{ Listener: &contour_api_v1alpha1.EnvoyListenerConfig{ - UseProxyProto: ref.To(true), - DisableAllowChunkedLength: ref.To(true), - DisableMergeSlashes: ref.To(true), - ConnectionBalancer: "yesplease", + UseProxyProto: ref.To(true), + DisableAllowChunkedLength: ref.To(true), + DisableMergeSlashes: ref.To(true), + ServerHeaderTransformation: contour_api_v1alpha1.PassThroughServerHeader, + ConnectionBalancer: "yesplease", TLS: &contour_api_v1alpha1.EnvoyTLS{ MinimumProtocolVersion: "1.7", CipherSuites: []string{ diff --git a/internal/envoy/v3/listener.go b/internal/envoy/v3/listener.go index 166bcacc7eb..0088140e2cd 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -41,6 +41,7 @@ import ( envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" + contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" "github.com/projectcontour/contour/internal/dag" "github.com/projectcontour/contour/internal/envoy" "github.com/projectcontour/contour/internal/protobuf" @@ -161,6 +162,7 @@ type httpConnectionManagerBuilder struct { codec HTTPVersionType // Note the zero value is AUTO, which is the default we want. allowChunkedLength bool mergeSlashes bool + serverHeaderTransformation http.HttpConnectionManager_ServerHeaderTransformation forwardClientCertificate *dag.ClientCertificateDetails numTrustedHops uint32 } @@ -241,6 +243,18 @@ func (b *httpConnectionManagerBuilder) MergeSlashes(enabled bool) *httpConnectio return b } +func (b *httpConnectionManagerBuilder) ServerHeaderTransformation(value contour_api_v1alpha1.ServerHeaderTransformationType) *httpConnectionManagerBuilder { + switch value { + case contour_api_v1alpha1.OverwriteServerHeader: + b.serverHeaderTransformation = http.HttpConnectionManager_OVERWRITE + case contour_api_v1alpha1.AppendIfAbsentServerHeader: + b.serverHeaderTransformation = http.HttpConnectionManager_APPEND_IF_ABSENT + case contour_api_v1alpha1.PassThroughServerHeader: + b.serverHeaderTransformation = http.HttpConnectionManager_PASS_THROUGH + } + return b +} + func (b *httpConnectionManagerBuilder) ForwardClientCertificate(details *dag.ClientCertificateDetails) *httpConnectionManagerBuilder { b.forwardClientCertificate = details return b @@ -453,8 +467,9 @@ func (b *httpConnectionManagerBuilder) Get() *envoy_listener_v3.Filter { }, // issue #1487 pass through X-Request-Id if provided. - PreserveExternalRequestId: true, - MergeSlashes: b.mergeSlashes, + PreserveExternalRequestId: true, + MergeSlashes: b.mergeSlashes, + ServerHeaderTransformation: b.serverHeaderTransformation, RequestTimeout: envoy.Timeout(b.requestTimeout), StreamIdleTimeout: envoy.Timeout(b.streamIdleTimeout), diff --git a/internal/envoy/v3/listener_test.go b/internal/envoy/v3/listener_test.go index fa0112ceb96..441e3427d20 100644 --- a/internal/envoy/v3/listener_test.go +++ b/internal/envoy/v3/listener_test.go @@ -604,6 +604,7 @@ func TestHTTPConnectionManager(t *testing.T) { connectionShutdownGracePeriod timeout.Setting allowChunkedLength bool mergeSlashes bool + serverHeaderTranformation v1alpha1.ServerHeaderTransformationType forwardClientCertificate *dag.ClientCertificateDetails xffNumTrustedHops uint32 want *envoy_listener_v3.Filter @@ -1118,6 +1119,56 @@ func TestHTTPConnectionManager(t *testing.T) { }, }, }, + "server header transform set to pass through": { + routename: "default/kuard", + accesslogger: FileAccessLogEnvoy("/dev/stdout", "", nil, v1alpha1.LogLevelInfo), + serverHeaderTranformation: v1alpha1.PassThroughServerHeader, + want: &envoy_listener_v3.Filter{ + Name: wellknown.HTTPConnectionManager, + ConfigType: &envoy_listener_v3.Filter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&http.HttpConnectionManager{ + StatPrefix: "default/kuard", + RouteSpecifier: &http.HttpConnectionManager_Rds{ + Rds: &http.Rds{ + RouteConfigName: "default/kuard", + ConfigSource: &envoy_core_v3.ConfigSource{ + ResourceApiVersion: envoy_core_v3.ApiVersion_V3, + ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_ApiConfigSource{ + ApiConfigSource: &envoy_core_v3.ApiConfigSource{ + ApiType: envoy_core_v3.ApiConfigSource_GRPC, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + GrpcServices: []*envoy_core_v3.GrpcService{{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: "contour", + Authority: "contour", + }, + }, + }}, + }, + }, + }, + }, + }, + HttpFilters: defaultHTTPFilters, + HttpProtocolOptions: &envoy_core_v3.Http1ProtocolOptions{ + // Enable support for HTTP/1.0 requests that carry + // a Host: header. See #537. + AcceptHttp_10: true, + }, + CommonHttpProtocolOptions: &envoy_core_v3.HttpProtocolOptions{}, + AccessLog: FileAccessLogEnvoy("/dev/stdout", "", nil, v1alpha1.LogLevelInfo), + UseRemoteAddress: wrapperspb.Bool(true), + NormalizePath: wrapperspb.Bool(true), + StripPortMode: &http.HttpConnectionManager_StripAnyHostPort{ + StripAnyHostPort: true, + }, + PreserveExternalRequestId: true, + ServerHeaderTransformation: http.HttpConnectionManager_PASS_THROUGH, + }), + }, + }, + }, "enable xfcc": { routename: "default/kuard", accesslogger: FileAccessLogEnvoy("/dev/stdout", "", nil, v1alpha1.LogLevelInfo), @@ -1308,6 +1359,7 @@ func TestHTTPConnectionManager(t *testing.T) { ConnectionShutdownGracePeriod(tc.connectionShutdownGracePeriod). AllowChunkedLength(tc.allowChunkedLength). MergeSlashes(tc.mergeSlashes). + ServerHeaderTransformation(tc.serverHeaderTranformation). NumTrustedHops(tc.xffNumTrustedHops). ForwardClientCertificate(tc.forwardClientCertificate). DefaultFilters(). diff --git a/internal/featuretests/v3/listeners_test.go b/internal/featuretests/v3/listeners_test.go index ffbca8b98a5..5d074e0880a 100644 --- a/internal/featuretests/v3/listeners_test.go +++ b/internal/featuretests/v3/listeners_test.go @@ -1295,3 +1295,57 @@ func TestHTTPProxyXffNumTrustedHops(t *testing.T) { TypeUrl: listenerType, }) } + +func TestHTTPProxyServerHeaderTransformation(t *testing.T) { + rh, c, done := setup(t, func(conf *xdscache_v3.ListenerConfig) { + conf.ServerHeaderTransformation = contour_api_v1alpha1.AppendIfAbsentServerHeader + }) + + defer done() + + rh.OnAdd(fixture.NewService("backend"). + WithPorts(v1.ServicePort{Name: "http", Port: 80})) + + // p1 is a httpproxy + p1 := &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "kuard.example.com", + }, + Routes: []contour_api_v1.Route{{ + Conditions: []contour_api_v1.MatchCondition{{ + Prefix: "/", + }}, + Services: []contour_api_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + } + rh.OnAdd(p1) + + // verify that the server-header-transformation has been set to append_if_absent. + httpListener := defaultHTTPListener() + + httpListener.FilterChains = envoy_v3.FilterChains(envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout", "", nil, contour_api_v1alpha1.LogLevelInfo)). + RequestTimeout(timeout.DurationSetting(0)). + ServerHeaderTransformation(contour_api_v1alpha1.AppendIfAbsentServerHeader). + DefaultFilters(). + Get()) + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + httpListener, + statsListener(), + ), + TypeUrl: listenerType, + }) +} diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index 5e6e31836ff..ddb096edb14 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -125,6 +125,9 @@ type ListenerConfig struct { // MergeSlashes toggles Envoy's non-standard merge_slashes path transformation option for all listeners. MergeSlashes bool + // ServerHeaderTransformation defines the action to be applied to the Server header on the response path. + ServerHeaderTransformation contour_api_v1alpha1.ServerHeaderTransformationType + // XffNumTrustedHops sets the number of additional ingress proxy hops from the // right side of the x-forwarded-for HTTP header to trust. XffNumTrustedHops uint32 @@ -389,6 +392,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { ConnectionShutdownGracePeriod(cfg.Timeouts.ConnectionShutdownGracePeriod). AllowChunkedLength(cfg.AllowChunkedLength). MergeSlashes(cfg.MergeSlashes). + ServerHeaderTransformation(cfg.ServerHeaderTransformation). NumTrustedHops(cfg.XffNumTrustedHops). AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). Get() @@ -449,6 +453,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { ConnectionShutdownGracePeriod(cfg.Timeouts.ConnectionShutdownGracePeriod). AllowChunkedLength(cfg.AllowChunkedLength). MergeSlashes(cfg.MergeSlashes). + ServerHeaderTransformation(cfg.ServerHeaderTransformation). NumTrustedHops(cfg.XffNumTrustedHops). AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). ForwardClientCertificate(forwardClientCertificate). @@ -515,6 +520,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { ConnectionShutdownGracePeriod(cfg.Timeouts.ConnectionShutdownGracePeriod). AllowChunkedLength(cfg.AllowChunkedLength). MergeSlashes(cfg.MergeSlashes). + ServerHeaderTransformation(cfg.ServerHeaderTransformation). NumTrustedHops(cfg.XffNumTrustedHops). AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). ForwardClientCertificate(forwardClientCertificate). diff --git a/internal/xdscache/v3/listener_test.go b/internal/xdscache/v3/listener_test.go index ab9f8b28959..ddab6b5213e 100644 --- a/internal/xdscache/v3/listener_test.go +++ b/internal/xdscache/v3/listener_test.go @@ -2616,6 +2616,7 @@ func TestListenerVisit(t *testing.T) { }, }, }, + want: listenermap(&envoy_listener_v3.Listener{ Name: ENVOY_HTTP_LISTENER, Address: envoy_v3.SocketAddress("0.0.0.0", 8080), @@ -2631,6 +2632,61 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, + "httpproxy with server_header_transformation set to pass through in listener config": { + ListenerConfig: ListenerConfig{ + ServerHeaderTransformation: v1alpha1.PassThroughServerHeader, + }, + objs: []interface{}{ + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_api_v1.Route{{ + Conditions: []contour_api_v1.MatchCondition{{ + Prefix: "/", + }}, + Services: []contour_api_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "http", + Protocol: "TCP", + Port: 80, + }}, + }, + }, + }, + + want: listenermap(&envoy_listener_v3.Listener{ + Name: ENVOY_HTTP_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8080), + FilterChains: envoy_v3.FilterChains( + envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName(ENVOY_HTTP_LISTENER). + MetricsPrefix(ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, v1alpha1.LogLevelInfo)). + DefaultFilters(). + ServerHeaderTransformation(v1alpha1.PassThroughServerHeader). + Get(), + ), + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + }), + }, "httpproxy with XffNumTrustedHops set in listener config": { ListenerConfig: ListenerConfig{ XffNumTrustedHops: 1, diff --git a/pkg/config/parameters.go b/pkg/config/parameters.go index db064e3dbe3..9757d1c4764 100644 --- a/pkg/config/parameters.go +++ b/pkg/config/parameters.go @@ -93,6 +93,22 @@ const IPv4ClusterDNSFamily ClusterDNSFamilyType = "v4" const IPv6ClusterDNSFamily ClusterDNSFamilyType = "v6" const AllClusterDNSFamily ClusterDNSFamilyType = "all" +// ServerHeaderTransformation defines the action to be applied to the Server header on the response path +type ServerHeaderTransformationType string + +func (s ServerHeaderTransformationType) Validate() error { + switch s { + case OverwriteServerHeader, AppendIfAbsentServerHeader, PassThroughServerHeader: + return nil + default: + return fmt.Errorf("invalid server header transformation %q", s) + } +} + +const OverwriteServerHeader ServerHeaderTransformationType = "overwrite" +const AppendIfAbsentServerHeader ServerHeaderTransformationType = "append_if_absent" +const PassThroughServerHeader ServerHeaderTransformationType = "pass_through" + // AccessLogType is the name of a supported access logging mechanism. type AccessLogType string @@ -489,6 +505,14 @@ type Parameters struct { // which strips duplicate slashes from request URL paths. DisableMergeSlashes bool `yaml:"disableMergeSlashes,omitempty"` + // Defines the action to be applied to the Server header on the response path. + // When configured as overwrite, overwrites any Server header with "envoy". + // When configured as append_if_absent, if a Server header is present, pass it through, otherwise set it to "envoy". + // When configured as pass_through, pass through the value of the Server header, and do not append a header if none is present. + // + // Contour's default is overwrite. + ServerHeaderTransformation ServerHeaderTransformationType `yaml:"serverHeaderTransformation,omitempty"` + // EnableExternalNameService allows processing of ExternalNameServices // Defaults to disabled for security reasons. // TODO(youngnick): put a link to the issue and CVE here. @@ -694,14 +718,15 @@ func Defaults() Parameters { Server: ServerParameters{ XDSServerType: ContourServerType, }, - IngressStatusAddress: "", - AccessLogFormat: DEFAULT_ACCESS_LOG_TYPE, - AccessLogFields: DefaultFields, - AccessLogLevel: LogLevelInfo, - TLS: TLSParameters{}, - DisablePermitInsecure: false, - DisableAllowChunkedLength: false, - DisableMergeSlashes: false, + IngressStatusAddress: "", + AccessLogFormat: DEFAULT_ACCESS_LOG_TYPE, + AccessLogFields: DefaultFields, + AccessLogLevel: LogLevelInfo, + TLS: TLSParameters{}, + DisablePermitInsecure: false, + DisableAllowChunkedLength: false, + DisableMergeSlashes: false, + ServerHeaderTransformation: OverwriteServerHeader, Timeouts: TimeoutParameters{ // This is chosen as a rough default to stop idle connections wasting resources, // without stopping slow connections from being terminated too quickly. diff --git a/pkg/config/parameters_test.go b/pkg/config/parameters_test.go index dedaa5efe46..d136eea3467 100644 --- a/pkg/config/parameters_test.go +++ b/pkg/config/parameters_test.go @@ -74,6 +74,7 @@ json-fields: - grpc_status - grpc_status_number accesslog-level: info +serverHeaderTransformation: overwrite timeouts: connection-idle-timeout: 60s connect-timeout: 2s @@ -128,6 +129,14 @@ func TestValidateClusterDNSFamilyType(t *testing.T) { assert.NoError(t, IPv6ClusterDNSFamily.Validate()) assert.NoError(t, AllClusterDNSFamily.Validate()) } +func TestValidateServerHeaderTranformationType(t *testing.T) { + assert.Error(t, ServerHeaderTransformationType("").Validate()) + assert.Error(t, ServerHeaderTransformationType("foo").Validate()) + + assert.NoError(t, OverwriteServerHeader.Validate()) + assert.NoError(t, AppendIfAbsentServerHeader.Validate()) + assert.NoError(t, PassThroughServerHeader.Validate()) +} func TestValidateHeadersPolicy(t *testing.T) { assert.Error(t, HeadersPolicy{ @@ -367,6 +376,7 @@ incluster: false disablePermitInsecure: false disableAllowChunkedLength: false disableMergeSlashes: false +serverHeaderTransformation: overwrite `, ) diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index 5d6eff31bec..be449e43201 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -5928,6 +5928,27 @@

EnvoyListenerConfig +serverHeaderTransformation +
+ + +ServerHeaderTransformationType + + + + +(Optional) +

Defines the action to be applied to the Server header on the response path. +When configured as overwrite, overwrites any Server header with “envoy”. +When configured as append_if_absent, if a Server header is present, pass it through, otherwise set it to “envoy”. +When configured as pass_through, pass through the value of the Server header, and do not append a header if none is present.

+

Values: overwrite (default), append_if_absent, pass_through

+

Other values will produce an error. +Contour’s default is overwrite.

+ + + + connectionBalancer
@@ -7421,6 +7442,36 @@

RateLimitServiceConfi +

ServerHeaderTransformationType +(string alias)

+

+(Appears on: +EnvoyListenerConfig) +

+

+

ServerHeaderTransformation defines the action to be applied to the Server header on the response path

+

+ + + + + + + + + + + + + + +
ValueDescription

"append_if_absent"

If no Server header is present, set it to “envoy”. +If a Server header is present, pass it through.

+

"overwrite"

Overwrite any Server header with “envoy”. +This is the default value.

+

"pass_through"

Pass through the value of the Server header, and do not append a header +if none is present.

+

TLS

diff --git a/site/content/docs/main/configuration.md b/site/content/docs/main/configuration.md index 8e188cde5b1..41f4dac2d9f 100644 --- a/site/content/docs/main/configuration.md +++ b/site/content/docs/main/configuration.md @@ -76,8 +76,8 @@ Where Contour settings can also be specified with command-line flags, the comman | debug | boolean | `false` | Enables debug logging. | | default-http-versions | string array | HTTP/1.1
HTTP/2 | This array specifies the HTTP versions that Contour should program Envoy to serve. HTTP versions are specified as strings of the form "HTTP/x", where "x" represents the version number. | | disableAllowChunkedLength | boolean | `false` | If this field is true, Contour will disable the RFC-compliant Envoy behavior to strip the `Content-Length` header if `Transfer-Encoding: chunked` is also set. This is an emergency off-switch to revert back to Envoy's default behavior in case of failures. -| disableMergeSlashes | boolean | `false` | This field disables Envoy's non-standard -merge_slashes path transformation behavior that strips duplicate slashes from request URL paths. +| disableMergeSlashes | boolean | `false` | This field disables Envoy's non-standard merge_slashes path transformation behavior that strips duplicate slashes from request URL paths. +| serverHeaderTransformation | string | `overwrite` | This field defines the action to be applied to the Server header on the response path. Values: `overwrite` (default), `append_if_absent`, `pass_through` | disablePermitInsecure | boolean | `false` | If this field is true, Contour will ignore `PermitInsecure` field in HTTPProxy documents. | | envoy-service-name | string | `envoy` | This sets the service name that will be inspected for address details to be applied to Ingress objects. | | envoy-service-namespace | string | `projectcontour` | This sets the namespace of the service that will be inspected for address details to be applied to Ingress objects. If the `CONTOUR_NAMESPACE` environment variable is present, Contour will populate this field with its value. |