Skip to content

Commit 48090ca

Browse files
Backport of [NET-11043] crd: support request normalization and header match options to prevent L7 intentions bypass into release/1.4.x (#4396)
backport of commit 2d3d34e Co-authored-by: Michael Zalimeni <[email protected]>
1 parent e7e743a commit 48090ca

16 files changed

+443
-60
lines changed

.changelog/4385.txt

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
```release-note:security
2+
crd: Add `http.incoming.requestNormalization` to the Mesh CRD to support configuring service traffic request normalization.
3+
```
4+
```release-note:security
5+
crd: Add `contains` and `ignoreCase` to the Intentions CRD to support configuring L7 Header intentions resilient to variable casing and multiple header values.
6+
```

acceptance/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/google/uuid v1.3.0
1010
github.com/gruntwork-io/terratest v0.46.7
1111
github.com/hashicorp/consul-k8s/control-plane v0.0.0-20240226161840-f3842c41cb2b
12-
github.com/hashicorp/consul/api v1.28.4
12+
github.com/hashicorp/consul/api v1.28.5
1313
github.com/hashicorp/consul/proto-public v0.6.1
1414
github.com/hashicorp/consul/sdk v0.16.0
1515
github.com/hashicorp/go-multierror v1.1.1

acceptance/go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ github.com/gruntwork-io/terratest v0.46.7 h1:oqGPBBO87SEsvBYaA0R5xOq+Lm2Xc5dmFVf
186186
github.com/gruntwork-io/terratest v0.46.7/go.mod h1:6gI5MlLeyF+SLwqocA5GBzcTix+XiuxCy1BPwKuT+WM=
187187
github.com/hashicorp/consul-k8s/control-plane v0.0.0-20240226161840-f3842c41cb2b h1:AdeWjUb+rxrRryC5ZHaL32oOZuxubOzV2q6oJ97UMT0=
188188
github.com/hashicorp/consul-k8s/control-plane v0.0.0-20240226161840-f3842c41cb2b/go.mod h1:TVaSJM7vYM/mtKGpVc/Lch53lrqLI9XAXJgy/gY8v4A=
189-
github.com/hashicorp/consul/api v1.28.4 h1:l/d3utZ7ITzIjd8OXznuSTbBnWFHGP2VE8WdGNFrhjE=
190-
github.com/hashicorp/consul/api v1.28.4/go.mod h1:GE+eY9pliiJiUXGchDpTaMVp27rSksHG+U+5jY+uJR4=
189+
github.com/hashicorp/consul/api v1.28.5 h1:WscnEAY/opiVDpXFEE8bajxtf/ZKypVIYeO1lG0m5nI=
190+
github.com/hashicorp/consul/api v1.28.5/go.mod h1:PbuTRqH1ejk45b1+duKXVQwnx+mS1f8gdna/zbQLNi4=
191191
github.com/hashicorp/consul/proto-public v0.6.1 h1:+uzH3olCrksXYWAYHKqK782CtK9scfqH+Unlw3UHhCg=
192192
github.com/hashicorp/consul/proto-public v0.6.1/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg=
193193
github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8=

charts/consul/templates/crd-meshes.yaml

+47-2
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,55 @@ spec:
6666
http:
6767
description: HTTP defines the HTTP configuration for the service mesh.
6868
properties:
69+
incoming:
70+
description: Incoming configures settings for incoming HTTP traffic
71+
to mesh proxies.
72+
properties:
73+
requestNormalization:
74+
description: |-
75+
RequestNormalizationMeshConfig contains options pertaining to the
76+
normalization of HTTP requests processed by mesh proxies.
77+
properties:
78+
headersWithUnderscoresAction:
79+
description: |-
80+
HeadersWithUnderscoresAction sets the value of the \`headers_with_underscores_action\` option in the Envoy
81+
listener's \`HttpConnectionManager\` under \`common_http_protocol_options\`. The default value of this option is
82+
empty, which is equivalent to \`ALLOW\`. Refer to the Envoy documentation for more information on available
83+
options.
84+
type: string
85+
insecureDisablePathNormalization:
86+
description: |-
87+
InsecureDisablePathNormalization sets the value of the \`normalize_path\` option in the Envoy listener's
88+
`HttpConnectionManager`. The default value is \`false\`. When set to \`true\` in Consul, \`normalize_path\` is
89+
set to \`false\` for the Envoy proxy. This parameter disables the normalization of request URL paths according to
90+
RFC 3986, conversion of \`\\\` to \`/\`, and decoding non-reserved %-encoded characters. When using L7 intentions
91+
with path match rules, we recommend enabling path normalization in order to avoid match rule circumvention with
92+
non-normalized path values.
93+
type: boolean
94+
mergeSlashes:
95+
description: |-
96+
MergeSlashes sets the value of the \`merge_slashes\` option in the Envoy listener's \`HttpConnectionManager\`.
97+
The default value is \`false\`. This option controls the normalization of request URL paths by merging
98+
consecutive \`/\` characters. This normalization is not part of RFC 3986. When using L7 intentions with path
99+
match rules, we recommend enabling this setting to avoid match rule circumvention through non-normalized path
100+
values, unless legitimate service traffic depends on allowing for repeat \`/\` characters, or upstream services
101+
are configured to differentiate between single and multiple slashes.
102+
type: boolean
103+
pathWithEscapedSlashesAction:
104+
description: |-
105+
PathWithEscapedSlashesAction sets the value of the \`path_with_escaped_slashes_action\` option in the Envoy
106+
listener's \`HttpConnectionManager\`. The default value of this option is empty, which is equivalent to
107+
\`IMPLEMENTATION_SPECIFIC_DEFAULT\`. This parameter controls the action taken in response to request URL paths
108+
with escaped slashes in the path. When using L7 intentions with path match rules, we recommend enabling this
109+
setting to avoid match rule circumvention through non-normalized path values, unless legitimate service traffic
110+
depends on allowing for escaped \`/\` or \`\\\` characters, or upstream services are configured to differentiate
111+
between escaped and unescaped slashes. Refer to the Envoy documentation for more information on available
112+
options.
113+
type: string
114+
type: object
115+
type: object
69116
sanitizeXForwardedClientCert:
70117
type: boolean
71-
required:
72-
- sanitizeXForwardedClientCert
73118
type: object
74119
peering:
75120
description: Peering defines the peering configuration for the service

charts/consul/templates/crd-serviceintentions.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,19 @@ spec:
168168
If more than one is configured all must match for the overall match to apply.
169169
items:
170170
properties:
171+
contains:
172+
description: Contains matches if the header
173+
with the given name contains this value.
174+
type: string
171175
exact:
172176
description: Exact matches if the header with
173177
the given name is this value.
174178
type: string
179+
ignoreCase:
180+
description: IgnoreCase ignores the case of
181+
the header value when matching with exact,
182+
prefix, suffix, or contains.
183+
type: boolean
175184
invert:
176185
description: Invert inverts the logic of the
177186
match.

cli/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ require (
9696
github.com/gorilla/websocket v1.5.0 // indirect
9797
github.com/gosuri/uitable v0.0.4 // indirect
9898
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
99-
github.com/hashicorp/consul/api v1.28.4 // indirect
99+
github.com/hashicorp/consul/api v1.28.5 // indirect
100100
github.com/hashicorp/consul/envoyextensions v0.6.2 // indirect
101101
github.com/hashicorp/errwrap v1.1.0 // indirect
102102
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect

cli/go.sum

+2-4
Original file line numberDiff line numberDiff line change
@@ -288,12 +288,10 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
288288
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
289289
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
290290
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
291-
github.com/hashicorp/consul/api v1.28.4 h1:l/d3utZ7ITzIjd8OXznuSTbBnWFHGP2VE8WdGNFrhjE=
292-
github.com/hashicorp/consul/api v1.28.4/go.mod h1:GE+eY9pliiJiUXGchDpTaMVp27rSksHG+U+5jY+uJR4=
291+
github.com/hashicorp/consul/api v1.28.5 h1:WscnEAY/opiVDpXFEE8bajxtf/ZKypVIYeO1lG0m5nI=
292+
github.com/hashicorp/consul/api v1.28.5/go.mod h1:PbuTRqH1ejk45b1+duKXVQwnx+mS1f8gdna/zbQLNi4=
293293
github.com/hashicorp/consul/envoyextensions v0.6.2 h1:ljrE/JMWJxw/AXY//plKp8IxAw9131Y2mX45l0lt/+E=
294294
github.com/hashicorp/consul/envoyextensions v0.6.2/go.mod h1:SWG7x/XUFlQje//G3sCAjn5gt4S+oy6t6l+RvfXTq38=
295-
github.com/hashicorp/consul/proto-public v0.6.1 h1:+uzH3olCrksXYWAYHKqK782CtK9scfqH+Unlw3UHhCg=
296-
github.com/hashicorp/consul/proto-public v0.6.1/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg=
297295
github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8=
298296
github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A=
299297
github.com/hashicorp/consul/troubleshoot v0.6.5 h1:RFL4FGzI1pSCqjc4E49hXyQDajyswC0zQmSKdpm1q8Q=

control-plane/api/v1alpha1/mesh_types.go

+124-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,18 @@ type MeshTLSConfig struct {
8787
}
8888

8989
type MeshHTTPConfig struct {
90-
SanitizeXForwardedClientCert bool `json:"sanitizeXForwardedClientCert"`
90+
SanitizeXForwardedClientCert bool `json:"sanitizeXForwardedClientCert,omitempty"`
91+
// Incoming configures settings for incoming HTTP traffic to mesh proxies.
92+
Incoming *MeshDirectionalHTTPConfig `json:"incoming,omitempty"`
93+
// There is not currently an outgoing MeshDirectionalHTTPConfig, as
94+
// the only required config for either direction at present is inbound
95+
// request normalization.
96+
}
97+
98+
// MeshDirectionalHTTPConfig holds mesh configuration specific to HTTP
99+
// requests for a given traffic direction.
100+
type MeshDirectionalHTTPConfig struct {
101+
RequestNormalization *RequestNormalizationMeshConfig `json:"requestNormalization,omitempty"`
91102
}
92103

93104
type PeeringMeshConfig struct {
@@ -117,6 +128,61 @@ type MeshDirectionalTLSConfig struct {
117128
CipherSuites []string `json:"cipherSuites,omitempty"`
118129
}
119130

131+
// RequestNormalizationMeshConfig contains options pertaining to the
132+
// normalization of HTTP requests processed by mesh proxies.
133+
type RequestNormalizationMeshConfig struct {
134+
// InsecureDisablePathNormalization sets the value of the \`normalize_path\` option in the Envoy listener's
135+
// `HttpConnectionManager`. The default value is \`false\`. When set to \`true\` in Consul, \`normalize_path\` is
136+
// set to \`false\` for the Envoy proxy. This parameter disables the normalization of request URL paths according to
137+
// RFC 3986, conversion of \`\\\` to \`/\`, and decoding non-reserved %-encoded characters. When using L7 intentions
138+
// with path match rules, we recommend enabling path normalization in order to avoid match rule circumvention with
139+
// non-normalized path values.
140+
InsecureDisablePathNormalization bool `json:"insecureDisablePathNormalization,omitempty"`
141+
// MergeSlashes sets the value of the \`merge_slashes\` option in the Envoy listener's \`HttpConnectionManager\`.
142+
// The default value is \`false\`. This option controls the normalization of request URL paths by merging
143+
// consecutive \`/\` characters. This normalization is not part of RFC 3986. When using L7 intentions with path
144+
// match rules, we recommend enabling this setting to avoid match rule circumvention through non-normalized path
145+
// values, unless legitimate service traffic depends on allowing for repeat \`/\` characters, or upstream services
146+
// are configured to differentiate between single and multiple slashes.
147+
MergeSlashes bool `json:"mergeSlashes,omitempty"`
148+
// PathWithEscapedSlashesAction sets the value of the \`path_with_escaped_slashes_action\` option in the Envoy
149+
// listener's \`HttpConnectionManager\`. The default value of this option is empty, which is equivalent to
150+
// \`IMPLEMENTATION_SPECIFIC_DEFAULT\`. This parameter controls the action taken in response to request URL paths
151+
// with escaped slashes in the path. When using L7 intentions with path match rules, we recommend enabling this
152+
// setting to avoid match rule circumvention through non-normalized path values, unless legitimate service traffic
153+
// depends on allowing for escaped \`/\` or \`\\\` characters, or upstream services are configured to differentiate
154+
// between escaped and unescaped slashes. Refer to the Envoy documentation for more information on available
155+
// options.
156+
PathWithEscapedSlashesAction string `json:"pathWithEscapedSlashesAction,omitempty"`
157+
// HeadersWithUnderscoresAction sets the value of the \`headers_with_underscores_action\` option in the Envoy
158+
// listener's \`HttpConnectionManager\` under \`common_http_protocol_options\`. The default value of this option is
159+
// empty, which is equivalent to \`ALLOW\`. Refer to the Envoy documentation for more information on available
160+
// options.
161+
HeadersWithUnderscoresAction string `json:"headersWithUnderscoresAction,omitempty"`
162+
}
163+
164+
// PathWithEscapedSlashesAction is an enum that defines the action to take when
165+
// a request path contains escaped slashes. It mirrors exactly the set of options
166+
// in Envoy's UriPathNormalizationOptions.PathWithEscapedSlashesAction enum.
167+
// See github.com/envoyproxy/go-control-plane envoy_http_v3.HttpConnectionManager_PathWithEscapedSlashesAction.
168+
const (
169+
PathWithEscapedSlashesActionDefault = "IMPLEMENTATION_SPECIFIC_DEFAULT"
170+
PathWithEscapedSlashesActionKeep = "KEEP_UNCHANGED"
171+
PathWithEscapedSlashesActionReject = "REJECT_REQUEST"
172+
PathWithEscapedSlashesActionUnescapeAndRedirect = "UNESCAPE_AND_REDIRECT"
173+
PathWithEscapedSlashesActionUnescapeAndForward = "UNESCAPE_AND_FORWARD"
174+
)
175+
176+
// HeadersWithUnderscoresAction is an enum that defines the action to take when
177+
// a request contains headers with underscores. It mirrors exactly the set of
178+
// options in Envoy's HttpProtocolOptions.HeadersWithUnderscoresAction enum.
179+
// See github.com/envoyproxy/go-control-plane envoy_core_v3.HttpProtocolOptions_HeadersWithUnderscoresAction.
180+
const (
181+
HeadersWithUnderscoresActionAllow = "ALLOW"
182+
HeadersWithUnderscoresActionRejectRequest = "REJECT_REQUEST"
183+
HeadersWithUnderscoresActionDropHeader = "DROP_HEADER"
184+
)
185+
120186
func (in *TransparentProxyMeshConfig) toConsul() capi.TransparentProxyMeshConfig {
121187
return capi.TransparentProxyMeshConfig{MeshDestinationsOnly: in.MeshDestinationsOnly}
122188
}
@@ -227,6 +293,11 @@ func (in *Mesh) Validate(consulMeta common.ConsulMeta) error {
227293

228294
errs = append(errs, in.Spec.TLS.validate(path.Child("tls"))...)
229295
errs = append(errs, in.Spec.Peering.validate(path.Child("peering"), consulMeta.PartitionsEnabled, consulMeta.Partition)...)
296+
if in.Spec.HTTP != nil &&
297+
in.Spec.HTTP.Incoming != nil &&
298+
in.Spec.HTTP.Incoming.RequestNormalization != nil {
299+
errs = append(errs, in.Spec.HTTP.Incoming.RequestNormalization.validate(path.Child("http", "incoming", "requestNormalization"))...)
300+
}
230301

231302
if len(errs) > 0 {
232303
return apierrors.NewInvalid(
@@ -252,6 +323,28 @@ func (in *MeshHTTPConfig) toConsul() *capi.MeshHTTPConfig {
252323
}
253324
return &capi.MeshHTTPConfig{
254325
SanitizeXForwardedClientCert: in.SanitizeXForwardedClientCert,
326+
Incoming: in.Incoming.toConsul(),
327+
}
328+
}
329+
330+
func (in *MeshDirectionalHTTPConfig) toConsul() *capi.MeshDirectionalHTTPConfig {
331+
if in == nil {
332+
return nil
333+
}
334+
return &capi.MeshDirectionalHTTPConfig{
335+
RequestNormalization: in.RequestNormalization.toConsul(),
336+
}
337+
}
338+
339+
func (in *RequestNormalizationMeshConfig) toConsul() *capi.RequestNormalizationMeshConfig {
340+
if in == nil {
341+
return nil
342+
}
343+
return &capi.RequestNormalizationMeshConfig{
344+
InsecureDisablePathNormalization: in.InsecureDisablePathNormalization,
345+
MergeSlashes: in.MergeSlashes,
346+
PathWithEscapedSlashesAction: in.PathWithEscapedSlashesAction,
347+
HeadersWithUnderscoresAction: in.HeadersWithUnderscoresAction,
255348
}
256349
}
257350

@@ -316,6 +409,36 @@ func (in *PeeringMeshConfig) validate(path *field.Path, partitionsEnabled bool,
316409
return errs
317410
}
318411

412+
func (in *RequestNormalizationMeshConfig) validate(path *field.Path) field.ErrorList {
413+
if in == nil {
414+
return nil
415+
}
416+
417+
var errs field.ErrorList
418+
pathWithEscapedSlashesActions := []string{
419+
PathWithEscapedSlashesActionDefault,
420+
PathWithEscapedSlashesActionKeep,
421+
PathWithEscapedSlashesActionReject,
422+
PathWithEscapedSlashesActionUnescapeAndRedirect,
423+
PathWithEscapedSlashesActionUnescapeAndForward,
424+
"",
425+
}
426+
headersWithUnderscoresActions := []string{
427+
HeadersWithUnderscoresActionAllow,
428+
HeadersWithUnderscoresActionRejectRequest,
429+
HeadersWithUnderscoresActionDropHeader,
430+
"",
431+
}
432+
433+
if !sliceContains(pathWithEscapedSlashesActions, in.PathWithEscapedSlashesAction) {
434+
errs = append(errs, field.Invalid(path.Child("pathWithEscapedSlashesAction"), in.PathWithEscapedSlashesAction, notInSliceMessage(pathWithEscapedSlashesActions)))
435+
}
436+
if !sliceContains(headersWithUnderscoresActions, in.HeadersWithUnderscoresAction) {
437+
errs = append(errs, field.Invalid(path.Child("headersWithUnderscoresAction"), in.HeadersWithUnderscoresAction, notInSliceMessage(headersWithUnderscoresActions)))
438+
}
439+
return errs
440+
}
441+
319442
// DefaultNamespaceFields has no behaviour here as meshes have no namespace specific fields.
320443
func (in *Mesh) DefaultNamespaceFields(_ common.ConsulMeta) {
321444
}

0 commit comments

Comments
 (0)