Skip to content

Commit

Permalink
crd: support mesh http.incoming.requestNormalization
Browse files Browse the repository at this point in the history
  • Loading branch information
zalimeni committed Oct 9, 2024
1 parent 2ce1e0b commit 10a45b5
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 0 deletions.
124 changes: 124 additions & 0 deletions control-plane/api/v1alpha1/mesh_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ type MeshTLSConfig struct {

type MeshHTTPConfig struct {
SanitizeXForwardedClientCert bool `json:"sanitizeXForwardedClientCert"`
// Incoming configures settings for incoming HTTP traffic to mesh proxies.
Incoming *MeshDirectionalHTTPConfig `json:"incoming,omitempty"`
// There is not currently an outgoing MeshDirectionalHTTPConfig, as
// the only required config for either direction at present is inbound
// request normalization.
}

// MeshDirectionalHTTPConfig holds mesh configuration specific to HTTP
// requests for a given traffic direction.
type MeshDirectionalHTTPConfig struct {
RequestNormalization *RequestNormalizationMeshConfig `json:"requestNormalization,omitempty"`
}

type PeeringMeshConfig struct {
Expand Down Expand Up @@ -117,6 +128,62 @@ type MeshDirectionalTLSConfig struct {
CipherSuites []string `json:"cipherSuites,omitempty"`
}

// RequestNormalizationMeshConfig contains options pertaining to the
// normalization of HTTP requests processed by mesh proxies.
type RequestNormalizationMeshConfig struct {
// InsecureDisablePathNormalization sets the value of the `normalize_path` option in the Envoy listener's
// `HttpConnectionManager`. If InsecureDisablePathNormalization is set to `true`, `normalize_path` will be set to
// `false` instead of Consul's default value of `true`. This disables the normalization of request URL paths
// according to RFC 3986, as well as converting `\` to `/` and decoding non-reserved %-encoded characters.
// If using L7 intentions with path match rules, it is strongly recommended to enable path normalization in order to
// avoid match rule circumvention via non-normalized path values.
// The default value of this option is `false` (recommended).
InsecureDisablePathNormalization bool `json:"insecureDisablePathNormalization,omitempty"`
// MergeSlashes sets the value of the `merge_slashes` option in the Envoy listener's `HttpConnectionManager`.
// It controls the normalization of request URL paths by merging consecutive `/` characters (not part of RFC 3986).
// If using L7 intentions with path match rules, it is recommended that this setting be enabled to avoid match rule
// circumvention via non-normalized path values, unless legitimate service traffic depends on allowing for repeat
// `/` characters or upstream services are configured to differentiate between single and multiple slashes.
// The default value of this option is `false`.
MergeSlashes bool `json:"mergeSlashes,omitempty"`
// PathWithEscapedSlashesAction sets the value of the `path_with_escaped_slashes_action` option in the Envoy
// listener's `HttpConnectionManager`.
// It controls the action taken in response to request URL paths with escaped slashes in the path.
// If using L7 intentions with path match rules, it is recommended that this be configured to avoid match rule
// circumvention via non-normalized path values, unless legitimate service traffic depends on allowing for escaped
// `/` or `\` characters or upstream services are configured to differentiate between escaped and unescaped slashes.
// The default value of this option is empty, which is equivalent to `IMPLEMENTATION_SPECIFIC_DEFAULT`. See Envoy
// docs for more information on available options.
PathWithEscapedSlashesAction string `json:"pathWithEscapedSlashesAction,omitempty"`
// HeadersWithUnderscoresAction sets the value of the `headers_with_underscores_action` option in the Envoy
// listener's `HttpConnectionManager` under `common_http_protocol_options`.
// The default value of this option is empty, which is equivalent to `ALLOW`. See Envoy docs for more information on
// available options.
HeadersWithUnderscoresAction string `json:"headersWithUnderscoresAction,omitempty"`
}

// PathWithEscapedSlashesAction is an enum that defines the action to take when
// a request path contains escaped slashes. It mirrors exactly the set of options
// in Envoy's UriPathNormalizationOptions.PathWithEscapedSlashesAction enum.
// See github.com/envoyproxy/go-control-plane envoy_http_v3.HttpConnectionManager_PathWithEscapedSlashesAction.
const (
PathWithEscapedSlashesActionDefault = "IMPLEMENTATION_SPECIFIC_DEFAULT"
PathWithEscapedSlashesActionKeep = "KEEP_UNCHANGED"
PathWithEscapedSlashesActionReject = "REJECT_REQUEST"
PathWithEscapedSlashesActionUnescapeAndRedirect = "UNESCAPE_AND_REDIRECT"
PathWithEscapedSlashesActionUnescapeAndForward = "UNESCAPE_AND_FORWARD"
)

// HeadersWithUnderscoresAction is an enum that defines the action to take when
// a request contains headers with underscores. It mirrors exactly the set of
// options in Envoy's HttpProtocolOptions.HeadersWithUnderscoresAction enum.
// See github.com/envoyproxy/go-control-plane envoy_core_v3.HttpProtocolOptions_HeadersWithUnderscoresAction.
const (
HeadersWithUnderscoresActionAllow = "ALLOW"
HeadersWithUnderscoresActionRejectRequest = "REJECT_REQUEST"
HeadersWithUnderscoresActionDropHeader = "DROP_HEADER"
)

func (in *TransparentProxyMeshConfig) toConsul() capi.TransparentProxyMeshConfig {
return capi.TransparentProxyMeshConfig{MeshDestinationsOnly: in.MeshDestinationsOnly}
}
Expand Down Expand Up @@ -227,6 +294,11 @@ func (in *Mesh) Validate(consulMeta common.ConsulMeta) error {

errs = append(errs, in.Spec.TLS.validate(path.Child("tls"))...)
errs = append(errs, in.Spec.Peering.validate(path.Child("peering"), consulMeta.PartitionsEnabled, consulMeta.Partition)...)
if in.Spec.HTTP != nil &&
in.Spec.HTTP.Incoming != nil &&
in.Spec.HTTP.Incoming.RequestNormalization != nil {
errs = append(errs, in.Spec.HTTP.Incoming.RequestNormalization.validate(path.Child("http", "incoming", "requestNormalization"))...)
}

if len(errs) > 0 {
return apierrors.NewInvalid(
Expand All @@ -252,6 +324,28 @@ func (in *MeshHTTPConfig) toConsul() *capi.MeshHTTPConfig {
}
return &capi.MeshHTTPConfig{
SanitizeXForwardedClientCert: in.SanitizeXForwardedClientCert,
Incoming: in.Incoming.toConsul(),
}
}

func (in *MeshDirectionalHTTPConfig) toConsul() *capi.MeshDirectionalHTTPConfig {
if in == nil {
return nil
}
return &capi.MeshDirectionalHTTPConfig{
RequestNormalization: in.RequestNormalization.toConsul(),
}
}

func (in *RequestNormalizationMeshConfig) toConsul() *capi.RequestNormalizationMeshConfig {
if in == nil {
return nil
}
return &capi.RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: in.InsecureDisablePathNormalization,
MergeSlashes: in.MergeSlashes,
PathWithEscapedSlashesAction: in.PathWithEscapedSlashesAction,
HeadersWithUnderscoresAction: in.HeadersWithUnderscoresAction,
}
}

Expand Down Expand Up @@ -316,6 +410,36 @@ func (in *PeeringMeshConfig) validate(path *field.Path, partitionsEnabled bool,
return errs
}

func (in *RequestNormalizationMeshConfig) validate(path *field.Path) field.ErrorList {
if in == nil {
return nil
}

var errs field.ErrorList
pathWithEscapedSlashesActions := []string{
PathWithEscapedSlashesActionDefault,
PathWithEscapedSlashesActionKeep,
PathWithEscapedSlashesActionReject,
PathWithEscapedSlashesActionUnescapeAndRedirect,
PathWithEscapedSlashesActionUnescapeAndForward,
"",
}
headersWithUnderscoresActions := []string{
HeadersWithUnderscoresActionAllow,
HeadersWithUnderscoresActionRejectRequest,
HeadersWithUnderscoresActionDropHeader,
"",
}

if !sliceContains(pathWithEscapedSlashesActions, in.PathWithEscapedSlashesAction) {
errs = append(errs, field.Invalid(path.Child("pathWithEscapedSlashesAction"), in.PathWithEscapedSlashesAction, notInSliceMessage(pathWithEscapedSlashesActions)))
}
if !sliceContains(headersWithUnderscoresActions, in.HeadersWithUnderscoresAction) {
errs = append(errs, field.Invalid(path.Child("headersWithUnderscoresAction"), in.HeadersWithUnderscoresAction, notInSliceMessage(headersWithUnderscoresActions)))
}
return errs
}

// DefaultNamespaceFields has no behaviour here as meshes have no namespace specific fields.
func (in *Mesh) DefaultNamespaceFields(_ common.ConsulMeta) {
}
112 changes: 112 additions & 0 deletions control-plane/api/v1alpha1/mesh_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ func TestMesh_MatchesConsul(t *testing.T) {
},
HTTP: &MeshHTTPConfig{
SanitizeXForwardedClientCert: true,
Incoming: &MeshDirectionalHTTPConfig{
RequestNormalization: &RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: true, // note: this is the opposite of the recommended default
MergeSlashes: true,
PathWithEscapedSlashesAction: "REJECT_REQUEST",
HeadersWithUnderscoresAction: "DROP_HEADER",
},
},
},
Peering: &PeeringMeshConfig{
PeerThroughMeshGateways: true,
Expand All @@ -90,6 +98,14 @@ func TestMesh_MatchesConsul(t *testing.T) {
},
HTTP: &capi.MeshHTTPConfig{
SanitizeXForwardedClientCert: true,
Incoming: &capi.MeshDirectionalHTTPConfig{
RequestNormalization: &capi.RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: true,
MergeSlashes: true,
PathWithEscapedSlashesAction: "REJECT_REQUEST",
HeadersWithUnderscoresAction: "DROP_HEADER",
},
},
},
Peering: &capi.PeeringMeshConfig{
PeerThroughMeshGateways: true,
Expand Down Expand Up @@ -168,6 +184,14 @@ func TestMesh_ToConsul(t *testing.T) {
},
HTTP: &MeshHTTPConfig{
SanitizeXForwardedClientCert: true,
Incoming: &MeshDirectionalHTTPConfig{
RequestNormalization: &RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: true, // note: this is the opposite of the recommended default
MergeSlashes: true,
PathWithEscapedSlashesAction: "REJECT_REQUEST",
HeadersWithUnderscoresAction: "DROP_HEADER",
},
},
},
Peering: &PeeringMeshConfig{
PeerThroughMeshGateways: true,
Expand All @@ -194,6 +218,14 @@ func TestMesh_ToConsul(t *testing.T) {
},
HTTP: &capi.MeshHTTPConfig{
SanitizeXForwardedClientCert: true,
Incoming: &capi.MeshDirectionalHTTPConfig{
RequestNormalization: &capi.RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: true,
MergeSlashes: true,
PathWithEscapedSlashesAction: "REJECT_REQUEST",
HeadersWithUnderscoresAction: "DROP_HEADER",
},
},
},
Peering: &capi.PeeringMeshConfig{
PeerThroughMeshGateways: true,
Expand Down Expand Up @@ -367,6 +399,76 @@ func TestMesh_Validate(t *testing.T) {
},
},
},
"http.incoming.requestNormalization.pathWithEscapedSlashesAction valid": {
input: &Mesh{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
},
Spec: MeshSpec{
HTTP: &MeshHTTPConfig{
Incoming: &MeshDirectionalHTTPConfig{
RequestNormalization: &RequestNormalizationMeshConfig{
PathWithEscapedSlashesAction: "UNESCAPE_AND_FORWARD",
},
},
},
},
},
},
"http.incoming.requestNormalization.pathWithEscapedSlashesAction invalid": {
input: &Mesh{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
},
Spec: MeshSpec{
HTTP: &MeshHTTPConfig{
Incoming: &MeshDirectionalHTTPConfig{
RequestNormalization: &RequestNormalizationMeshConfig{
PathWithEscapedSlashesAction: "foo",
},
},
},
},
},
expectedErrMsgs: []string{
`spec.http.incoming.requestNormalization.pathWithEscapedSlashesAction: Invalid value: "foo": must be one of "IMPLEMENTATION_SPECIFIC_DEFAULT", "KEEP_UNCHANGED", "REJECT_REQUEST", "UNESCAPE_AND_REDIRECT", "UNESCAPE_AND_FORWARD", ""`,
},
},
"http.incoming.requestNormalization.headerWithUnderscoresAction valid": {
input: &Mesh{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
},
Spec: MeshSpec{
HTTP: &MeshHTTPConfig{
Incoming: &MeshDirectionalHTTPConfig{
RequestNormalization: &RequestNormalizationMeshConfig{
HeadersWithUnderscoresAction: "REJECT_REQUEST",
},
},
},
},
},
},
"http.incoming.requestNormalization.headersWithUnderscoresAction invalid": {
input: &Mesh{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
},
Spec: MeshSpec{
HTTP: &MeshHTTPConfig{
Incoming: &MeshDirectionalHTTPConfig{
RequestNormalization: &RequestNormalizationMeshConfig{
HeadersWithUnderscoresAction: "bar",
},
},
},
},
},
expectedErrMsgs: []string{
`spec.http.incoming.requestNormalization.headersWithUnderscoresAction: Invalid value: "bar": must be one of "ALLOW", "REJECT_REQUEST", "DROP_HEADER", ""`,
},
},
"multiple errors": {
input: &Mesh{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -386,6 +488,14 @@ func TestMesh_Validate(t *testing.T) {
Peering: &PeeringMeshConfig{
PeerThroughMeshGateways: true,
},
HTTP: &MeshHTTPConfig{
Incoming: &MeshDirectionalHTTPConfig{
RequestNormalization: &RequestNormalizationMeshConfig{
PathWithEscapedSlashesAction: "foo",
HeadersWithUnderscoresAction: "bar",
},
},
},
},
},
consulMeta: common.ConsulMeta{
Expand All @@ -398,6 +508,8 @@ func TestMesh_Validate(t *testing.T) {
`spec.tls.outgoing.tlsMinVersion: Invalid value: "foo": must be one of "TLS_AUTO", "TLSv1_0", "TLSv1_1", "TLSv1_2", "TLSv1_3", ""`,
`spec.tls.outgoing.tlsMaxVersion: Invalid value: "bar": must be one of "TLS_AUTO", "TLSv1_0", "TLSv1_1", "TLSv1_2", "TLSv1_3", ""`,
`spec.peering.peerThroughMeshGateways: Forbidden: "peerThroughMeshGateways" is only valid in the "default" partition`,
`spec.http.incoming.requestNormalization.pathWithEscapedSlashesAction: Invalid value: "foo": must be one of "IMPLEMENTATION_SPECIFIC_DEFAULT", "KEEP_UNCHANGED", "REJECT_REQUEST", "UNESCAPE_AND_REDIRECT", "UNESCAPE_AND_FORWARD", ""`,
`spec.http.incoming.requestNormalization.headersWithUnderscoresAction: Invalid value: "bar": must be one of "ALLOW", "REJECT_REQUEST", "DROP_HEADER", ""`,
},
},
}
Expand Down

0 comments on commit 10a45b5

Please sign in to comment.