consul/agent/structs/config_entry_mesh_test.go
Michael Zalimeni d9206fc7e2
[NET-1151 NET-11228] security: Add request normalization and header match options to prevent L7 intentions bypass (#21816)
mesh: add options for HTTP incoming request normalization

Expose global mesh configuration to enforce inbound HTTP request
normalization on mesh traffic via Envoy xDS config.

mesh: enable inbound URL path normalization by default

mesh: add support for L7 header match contains and ignore_case

Enable partial string and case-insensitive matching in L7 intentions
header match rules.

ui: support L7 header match contains and ignore_case

Co-authored-by: Phil Renaud <phil@riotindustries.com>

test: add request normalization integration bats tests

Add both "positive" and "negative" test suites, showing normalization in
action as well as expected results when it is not enabled, for the same
set of test cases.

Also add some alternative service container test helpers for verifying
raw HTTP request paths, which is difficult to do with Fortio.

docs: update security and reference docs for L7 intentions bypass prevention

- Update security docs with best practices for service intentions
  configuration
- Update configuration entry references for mesh and intentions to
  reflect new values and add guidance on usage
2024-10-16 12:23:33 -04:00

211 lines
6.0 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMeshConfigEntry_PeerThroughMeshGateways(t *testing.T) {
tests := map[string]struct {
input *MeshConfigEntry
want bool
}{
"nil entry": {
input: nil,
want: false,
},
"nil peering config": {
input: &MeshConfigEntry{
Peering: nil,
},
want: false,
},
"not peering through gateways": {
input: &MeshConfigEntry{
Peering: &PeeringMeshConfig{
PeerThroughMeshGateways: false,
},
},
want: false,
},
"peering through gateways": {
input: &MeshConfigEntry{
Peering: &PeeringMeshConfig{
PeerThroughMeshGateways: true,
},
},
want: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
assert.Equalf(t, tc.want, tc.input.PeerThroughMeshGateways(), "PeerThroughMeshGateways()")
})
}
}
func TestMeshConfigEntry_GetHTTPIncomingRequestNormalization(t *testing.T) {
tests := map[string]struct {
input *MeshConfigEntry
want *RequestNormalizationMeshConfig
}{
// Ensure nil is gracefully handled at each level of config path.
"nil entry": {
input: nil,
want: nil,
},
"nil http config": {
input: &MeshConfigEntry{
HTTP: nil,
},
want: nil,
},
"nil http incoming config": {
input: &MeshConfigEntry{
HTTP: &MeshHTTPConfig{
Incoming: nil,
},
},
want: nil,
},
"nil http incoming request normalization config": {
input: &MeshConfigEntry{
HTTP: &MeshHTTPConfig{
Incoming: &MeshDirectionalHTTPConfig{
RequestNormalization: nil,
},
},
},
want: nil,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tc.want, tc.input.GetHTTPIncomingRequestNormalization())
})
}
}
func TestMeshConfigEntry_RequestNormalizationMeshConfig(t *testing.T) {
tests := map[string]struct {
input *RequestNormalizationMeshConfig
getFn func(*RequestNormalizationMeshConfig) any
want any
}{
// Ensure defaults are returned when config is not set.
"nil entry gets false GetInsecureDisablePathNormalization": {
input: nil,
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetInsecureDisablePathNormalization()
},
want: false,
},
"nil entry gets false GetMergeSlashes": {
input: nil,
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetMergeSlashes()
},
want: false,
},
"nil entry gets default GetPathWithEscapedSlashesAction": {
input: nil,
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetPathWithEscapedSlashesAction()
},
want: PathWithEscapedSlashesAction("IMPLEMENTATION_SPECIFIC_DEFAULT"),
},
"nil entry gets default GetHeadersWithUnderscoresAction": {
input: nil,
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetHeadersWithUnderscoresAction()
},
want: HeadersWithUnderscoresAction("ALLOW"),
},
"empty entry gets default GetPathWithEscapedSlashesAction": {
input: &RequestNormalizationMeshConfig{},
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetPathWithEscapedSlashesAction()
},
want: PathWithEscapedSlashesAction("IMPLEMENTATION_SPECIFIC_DEFAULT"),
},
"empty entry gets default GetHeadersWithUnderscoresAction": {
input: &RequestNormalizationMeshConfig{},
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetHeadersWithUnderscoresAction()
},
want: HeadersWithUnderscoresAction("ALLOW"),
},
// Ensure values are returned when set.
"non-default entry gets expected InsecureDisablePathNormalization": {
input: &RequestNormalizationMeshConfig{InsecureDisablePathNormalization: true},
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetInsecureDisablePathNormalization()
},
want: true,
},
"non-default entry gets expected MergeSlashes": {
input: &RequestNormalizationMeshConfig{MergeSlashes: true},
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetMergeSlashes()
},
want: true,
},
"non-default entry gets expected PathWithEscapedSlashesAction": {
input: &RequestNormalizationMeshConfig{PathWithEscapedSlashesAction: "UNESCAPE_AND_FORWARD"},
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetPathWithEscapedSlashesAction()
},
want: PathWithEscapedSlashesAction("UNESCAPE_AND_FORWARD"),
},
"non-default entry gets expected HeadersWithUnderscoresAction": {
input: &RequestNormalizationMeshConfig{HeadersWithUnderscoresAction: "REJECT_REQUEST"},
getFn: func(c *RequestNormalizationMeshConfig) any {
return c.GetHeadersWithUnderscoresAction()
},
want: HeadersWithUnderscoresAction("REJECT_REQUEST"),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tc.want, tc.getFn(tc.input))
})
}
}
func TestMeshConfigEntry_validateRequestNormalizationMeshConfig(t *testing.T) {
tests := map[string]struct {
input *RequestNormalizationMeshConfig
wantErr string
}{
"nil entry is valid": {
input: nil,
wantErr: "",
},
"invalid PathWithEscapedSlashesAction is rejected": {
input: &RequestNormalizationMeshConfig{
PathWithEscapedSlashesAction: PathWithEscapedSlashesAction("INVALID"),
},
wantErr: "no matching PathWithEscapedSlashesAction value found for INVALID, please specify one of [IMPLEMENTATION_SPECIFIC_DEFAULT, KEEP_UNCHANGED, REJECT_REQUEST, UNESCAPE_AND_REDIRECT, UNESCAPE_AND_FORWARD]",
},
"invalid HeadersWithUnderscoresAction is rejected": {
input: &RequestNormalizationMeshConfig{
HeadersWithUnderscoresAction: HeadersWithUnderscoresAction("INVALID"),
},
wantErr: "no matching HeadersWithUnderscoresAction value found for INVALID, please specify one of [ALLOW, REJECT_REQUEST, DROP_HEADER]",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
if tc.wantErr == "" {
assert.NoError(t, validateRequestNormalizationMeshConfig(tc.input))
} else {
assert.EqualError(t, validateRequestNormalizationMeshConfig(tc.input), tc.wantErr)
}
})
}
}