[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
pull/20887/head^2
Michael Zalimeni 2 months ago committed by GitHub
parent 3370f6b250
commit d9206fc7e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,9 @@
```release-note:security
mesh: Add `http.incoming.requestNormalization` to Mesh configuration entry to support inbound service traffic request normalization. This resolves [CVE-2024-10005](https://nvd.nist.gov/vuln/detail/CVE-2024-10005) and [CVE-2024-10006](https://nvd.nist.gov/vuln/detail/CVE-2024-10006).
```
```release-note:security
mesh: Add `contains` and `ignoreCase` to L7 Intentions HTTP header matching criteria to support configuration resilient to variable casing and multiple values. This resolves [CVE-2024-10006](https://nvd.nist.gov/vuln/detail/CVE-2024-10006).
```
```release-note:breaking-change
mesh: Enable Envoy `HttpConnectionManager.normalize_path` by default on inbound traffic to mesh proxies. This resolves [CVE-2024-10005](https://nvd.nist.gov/vuln/detail/CVE-2024-10005).
```

@ -426,13 +426,15 @@ func (p *IntentionHTTPPermission) Clone() *IntentionHTTPPermission {
}
type IntentionHTTPHeaderPermission struct {
Name string
Present bool `json:",omitempty"`
Exact string `json:",omitempty"`
Prefix string `json:",omitempty"`
Suffix string `json:",omitempty"`
Regex string `json:",omitempty"`
Invert bool `json:",omitempty"`
Name string
Present bool `json:",omitempty"`
Exact string `json:",omitempty"`
Prefix string `json:",omitempty"`
Suffix string `json:",omitempty"`
Contains string `json:",omitempty"`
Regex string `json:",omitempty"`
Invert bool `json:",omitempty"`
IgnoreCase bool `json:",omitempty" alias:"ignore_case"`
}
func cloneStringStringMap(m map[string]string) map[string]string {
@ -880,8 +882,14 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
if hdr.Suffix != "" {
hdrParts++
}
if hdr.Contains != "" {
hdrParts++
}
if hdrParts != 1 {
return fmt.Errorf(errorPrefix+".Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex", i, j, k)
return fmt.Errorf(errorPrefix+".Header[%d] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex", i, j, k)
}
if hdr.IgnoreCase && (hdr.Present || hdr.Regex != "") {
return fmt.Errorf(errorPrefix+".Header[%d] should set one of Exact, Prefix, Suffix, or Contains when using IgnoreCase", i, j, k)
}
permParts++
}

@ -574,323 +574,164 @@ func TestServiceIntentionsConfigEntry(t *testing.T) {
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] missing required Name field`,
},
"permission header has too many parts (1)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
Exact: "foo",
// Regex: "foo",
// Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
// Regex: "foo",
// Prefix: "foo",
Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (2)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
// Exact: "foo",
Regex: "foo",
// Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
Regex: "foo",
Prefix: "foo",
// Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (3)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
// Exact: "foo",
// Regex: "foo",
Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
// Regex: "foo",
Prefix: "foo",
Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (4)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
// Exact: "foo",
// Regex: "foo",
// Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
// Regex: "foo",
Prefix: "foo",
Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (5)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
Regex: "foo",
// Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
Exact: "foo",
Regex: "foo",
Prefix: "foo",
Suffix: "foo",
Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (6)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
// Regex: "foo",
Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
Exact: "foo",
// Regex: "foo",
Prefix: "foo",
Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (7)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
// Regex: "foo",
// Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
Exact: "foo",
// Regex: "foo",
Prefix: "foo",
Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (8)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
Regex: "foo",
Prefix: "foo",
// Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
Regex: "foo",
Prefix: "foo",
// Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (9)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
Regex: "foo",
// Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
Regex: "foo",
// Prefix: "foo",
Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header has too many parts (10)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
// Regex: "foo",
Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
// Present: true,
// Exact: "foo",
// Regex: "foo",
Prefix: "foo",
Suffix: "foo",
// Contains: "foo",
},
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, Contains, or Regex`,
},
"permission header invalid ignore case (1)": {
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
IgnoreCase: true,
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should set one of Exact, Prefix, Suffix, or Contains when using IgnoreCase`,
},
"permission header has too many parts (11)": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: []IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Present: true,
Exact: "foo",
Regex: "foo",
Prefix: "foo",
Suffix: "foo",
},
},
},
},
},
},
"permission header invalid ignore case (2)": {
entry: httpHeaderPermissionEntry([]IntentionHTTPHeaderPermission{
{
Name: "x-abc",
Regex: "qux",
IgnoreCase: true,
},
},
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`,
}),
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should set one of Exact, Prefix, Suffix, or Contains when using IgnoreCase`,
},
"permission invalid method": {
entry: &ServiceIntentionsConfigEntry{
@ -1677,3 +1518,25 @@ func TestMigrateIntentions(t *testing.T) {
})
}
}
// httpHeaderPermissionEntry is a helper to generate a ServiceIntentionsConfigEntry for testing
// IntentionHTTPHeaderPermission values.
func httpHeaderPermissionEntry(header []IntentionHTTPHeaderPermission) *ServiceIntentionsConfigEntry {
return &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
Permissions: []*IntentionPermission{
{
Action: IntentionActionAllow,
HTTP: &IntentionHTTPPermission{
Header: header,
},
},
},
},
},
}
}

@ -6,6 +6,7 @@ package structs
import (
"encoding/json"
"fmt"
"strings"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/types"
@ -72,6 +73,17 @@ type MeshDirectionalTLSConfig struct {
type MeshHTTPConfig struct {
SanitizeXForwardedClientCert bool `alias:"sanitize_x_forwarded_client_cert"`
// Incoming configures settings for incoming HTTP traffic to mesh proxies.
Incoming *MeshDirectionalHTTPConfig `json:",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:",omitempty" alias:"request_normalization"`
}
// PeeringMeshConfig contains cluster-wide options pertaining to peering.
@ -84,6 +96,104 @@ type PeeringMeshConfig struct {
PeerThroughMeshGateways bool `alias:"peer_through_mesh_gateways"`
}
// 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`. The default value is \`false\`. When set to \`true\` in Consul, \`normalize_path\` is
// set to \`false\` for the Envoy proxy. This parameter disables the normalization of request URL paths according to
// RFC 3986, conversion of \`\\\` to \`/\`, and decoding non-reserved %-encoded characters. When using L7 intentions
// with path match rules, we recommend enabling path normalization in order to avoid match rule circumvention with
// non-normalized path values.
InsecureDisablePathNormalization bool `alias:"insecure_disable_path_normalization"`
// MergeSlashes sets the value of the \`merge_slashes\` option in the Envoy listener's \`HttpConnectionManager\`.
// The default value is \`false\`. This option controls the normalization of request URL paths by merging
// consecutive \`/\` characters. This normalization is not part of RFC 3986. When using L7 intentions with path
// match rules, we recommend enabling this setting to avoid match rule circumvention through 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.
MergeSlashes bool `alias:"merge_slashes"`
// PathWithEscapedSlashesAction sets the value of the \`path_with_escaped_slashes_action\` option in the Envoy
// listener's \`HttpConnectionManager\`. The default value of this option is empty, which is equivalent to
// \`IMPLEMENTATION_SPECIFIC_DEFAULT\`. This parameter controls the action taken in response to request URL paths
// with escaped slashes in the path. When using L7 intentions with path match rules, we recommend enabling this
// setting to avoid match rule circumvention through 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. Refer to the Envoy documentation for more information on available
// options.
PathWithEscapedSlashesAction PathWithEscapedSlashesAction `alias:"path_with_escaped_slashes_action"`
// 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\`. Refer to the Envoy documentation for more information on available
// options.
HeadersWithUnderscoresAction HeadersWithUnderscoresAction `alias:"headers_with_underscores_action"`
}
// 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.
type PathWithEscapedSlashesAction string
// See github.com/envoyproxy/go-control-plane envoy_http_v3.HttpConnectionManager_PathWithEscapedSlashesAction.
const (
PathWithEscapedSlashesActionDefault PathWithEscapedSlashesAction = "IMPLEMENTATION_SPECIFIC_DEFAULT"
PathWithEscapedSlashesActionKeep PathWithEscapedSlashesAction = "KEEP_UNCHANGED"
PathWithEscapedSlashesActionReject PathWithEscapedSlashesAction = "REJECT_REQUEST"
PathWithEscapedSlashesActionUnescapeAndRedirect PathWithEscapedSlashesAction = "UNESCAPE_AND_REDIRECT"
PathWithEscapedSlashesActionUnescapeAndForward PathWithEscapedSlashesAction = "UNESCAPE_AND_FORWARD"
)
// PathWithEscapedSlashesActionStrings returns an ordered slice of all PathWithEscapedSlashesAction values as strings.
func PathWithEscapedSlashesActionStrings() []string {
return []string{
string(PathWithEscapedSlashesActionDefault),
string(PathWithEscapedSlashesActionKeep),
string(PathWithEscapedSlashesActionReject),
string(PathWithEscapedSlashesActionUnescapeAndRedirect),
string(PathWithEscapedSlashesActionUnescapeAndForward),
}
}
// pathWithEscapedSlashesActions contains the canonical set of PathWithEscapedSlashesActionValues values.
var pathWithEscapedSlashesActions = (func() map[PathWithEscapedSlashesAction]struct{} {
m := make(map[PathWithEscapedSlashesAction]struct{})
for _, v := range PathWithEscapedSlashesActionStrings() {
m[PathWithEscapedSlashesAction(v)] = struct{}{}
}
return m
})()
// 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.
type HeadersWithUnderscoresAction string
// See github.com/envoyproxy/go-control-plane envoy_core_v3.HttpProtocolOptions_HeadersWithUnderscoresAction.
const (
HeadersWithUnderscoresActionAllow HeadersWithUnderscoresAction = "ALLOW"
HeadersWithUnderscoresActionRejectRequest HeadersWithUnderscoresAction = "REJECT_REQUEST"
HeadersWithUnderscoresActionDropHeader HeadersWithUnderscoresAction = "DROP_HEADER"
)
// HeadersWithUnderscoresActionStrings returns an ordered slice of all HeadersWithUnderscoresAction values as strings
// for use in returning validation errors.
func HeadersWithUnderscoresActionStrings() []string {
return []string{
string(HeadersWithUnderscoresActionAllow),
string(HeadersWithUnderscoresActionRejectRequest),
string(HeadersWithUnderscoresActionDropHeader),
}
}
// headersWithUnderscoresActions contains the canonical set of HeadersWithUnderscoresAction values.
var headersWithUnderscoresActions = (func() map[HeadersWithUnderscoresAction]struct{} {
m := make(map[HeadersWithUnderscoresAction]struct{})
for _, v := range HeadersWithUnderscoresActionStrings() {
m[HeadersWithUnderscoresAction(v)] = struct{}{}
}
return m
})()
func (e *MeshConfigEntry) GetKind() string {
return MeshConfig
}
@ -141,6 +251,10 @@ func (e *MeshConfigEntry) Validate() error {
}
}
if err := validateRequestNormalizationMeshConfig(e.GetHTTPIncomingRequestNormalization()); err != nil {
return fmt.Errorf("error in HTTP incoming request normalization configuration: %v", err)
}
return e.validateEnterpriseMeta()
}
@ -193,6 +307,61 @@ func (e *MeshConfigEntry) PeerThroughMeshGateways() bool {
return e.Peering.PeerThroughMeshGateways
}
func (e *MeshConfigEntry) GetHTTP() *MeshHTTPConfig {
if e == nil {
return nil
}
return e.HTTP
}
func (e *MeshHTTPConfig) GetIncoming() *MeshDirectionalHTTPConfig {
if e == nil {
return nil
}
return e.Incoming
}
func (e *MeshDirectionalHTTPConfig) GetRequestNormalization() *RequestNormalizationMeshConfig {
if e == nil {
return nil
}
return e.RequestNormalization
}
// GetHTTPIncomingRequestNormalization is a convenience accessor for mesh.http.incoming.request_normalization
// since no other fields currently exist under mesh.http.incoming.
func (e *MeshConfigEntry) GetHTTPIncomingRequestNormalization() *RequestNormalizationMeshConfig {
return e.GetHTTP().GetIncoming().GetRequestNormalization()
}
func (r *RequestNormalizationMeshConfig) GetInsecureDisablePathNormalization() bool {
if r == nil {
return false
}
return r.InsecureDisablePathNormalization
}
func (r *RequestNormalizationMeshConfig) GetMergeSlashes() bool {
if r == nil {
return false
}
return r.MergeSlashes
}
func (r *RequestNormalizationMeshConfig) GetPathWithEscapedSlashesAction() PathWithEscapedSlashesAction {
if r == nil || r.PathWithEscapedSlashesAction == "" {
return PathWithEscapedSlashesActionDefault
}
return r.PathWithEscapedSlashesAction
}
func (r *RequestNormalizationMeshConfig) GetHeadersWithUnderscoresAction() HeadersWithUnderscoresAction {
if r == nil || r.HeadersWithUnderscoresAction == "" {
return HeadersWithUnderscoresActionAllow
}
return r.HeadersWithUnderscoresAction
}
func validateMeshDirectionalTLSConfig(cfg *MeshDirectionalTLSConfig) error {
if cfg == nil {
return nil
@ -237,3 +406,36 @@ func validateTLSConfig(
return nil
}
func validateRequestNormalizationMeshConfig(cfg *RequestNormalizationMeshConfig) error {
if cfg == nil {
return nil
}
if err := validatePathWithEscapedSlashesAction(cfg.PathWithEscapedSlashesAction); err != nil {
return err
}
if err := validateHeadersWithUnderscoresAction(cfg.HeadersWithUnderscoresAction); err != nil {
return err
}
return nil
}
func validatePathWithEscapedSlashesAction(v PathWithEscapedSlashesAction) error {
if v == "" {
return nil
}
if _, ok := pathWithEscapedSlashesActions[v]; !ok {
return fmt.Errorf("no matching PathWithEscapedSlashesAction value found for %s, please specify one of [%s]", string(v), strings.Join(PathWithEscapedSlashesActionStrings(), ", "))
}
return nil
}
func validateHeadersWithUnderscoresAction(v HeadersWithUnderscoresAction) error {
if v == "" {
return nil
}
if _, ok := headersWithUnderscoresActions[v]; !ok {
return fmt.Errorf("no matching HeadersWithUnderscoresAction value found for %s, please specify one of [%s]", string(v), strings.Join(HeadersWithUnderscoresActionStrings(), ", "))
}
return nil
}

@ -47,3 +47,164 @@ func TestMeshConfigEntry_PeerThroughMeshGateways(t *testing.T) {
})
}
}
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)
}
})
}
}

@ -1910,6 +1910,10 @@ func TestDecodeConfigEntry(t *testing.T) {
name = "hdr-suffix"
suffix = "suffix"
},
{
name = "hdr-contains"
contains = "contains"
},
{
name = "hdr-regex"
regex = "regex"
@ -1918,7 +1922,12 @@ func TestDecodeConfigEntry(t *testing.T) {
name = "hdr-absent"
present = true
invert = true
}
},
{
name = "hdr-ignore-case"
exact = "exact"
ignore_case = true
},
]
}
},
@ -1987,6 +1996,10 @@ func TestDecodeConfigEntry(t *testing.T) {
Name = "hdr-suffix"
Suffix = "suffix"
},
{
Name = "hdr-contains"
Contains = "contains"
},
{
Name = "hdr-regex"
Regex = "regex"
@ -1995,6 +2008,11 @@ func TestDecodeConfigEntry(t *testing.T) {
Name = "hdr-absent"
Present = true
Invert = true
},
{
Name = "hdr-ignore-case"
Exact = "exact"
IgnoreCase = true
}
]
}
@ -2064,6 +2082,10 @@ func TestDecodeConfigEntry(t *testing.T) {
Name: "hdr-suffix",
Suffix: "suffix",
},
{
Name: "hdr-contains",
Contains: "contains",
},
{
Name: "hdr-regex",
Regex: "regex",
@ -2073,6 +2095,11 @@ func TestDecodeConfigEntry(t *testing.T) {
Present: true,
Invert: true,
},
{
Name: "hdr-ignore-case",
Exact: "exact",
IgnoreCase: true,
},
},
},
},
@ -2134,7 +2161,7 @@ func TestDecodeConfigEntry(t *testing.T) {
},
},
{
name: "mesh",
name: "mesh: kitchen sink",
snake: `
kind = "mesh"
meta {
@ -2145,6 +2172,7 @@ func TestDecodeConfigEntry(t *testing.T) {
mesh_destinations_only = true
}
allow_enabling_permissive_mutual_tls = true
validate_clusters = true
tls {
incoming {
tls_min_version = "TLSv1_1"
@ -2163,9 +2191,17 @@ func TestDecodeConfigEntry(t *testing.T) {
]
}
}
http {
sanitize_x_forwarded_client_cert = true
}
http {
sanitize_x_forwarded_client_cert = true
incoming {
request_normalization {
insecure_disable_path_normalization = true
merge_slashes = true
path_with_escaped_slashes_action = "UNESCAPE_AND_FORWARD"
headers_with_underscores_action = "DROP_HEADER"
}
}
}
peering {
peer_through_mesh_gateways = true
}
@ -2180,6 +2216,7 @@ func TestDecodeConfigEntry(t *testing.T) {
MeshDestinationsOnly = true
}
AllowEnablingPermissiveMutualTLS = true
ValidateClusters = true
TLS {
Incoming {
TLSMinVersion = "TLSv1_1"
@ -2198,9 +2235,17 @@ func TestDecodeConfigEntry(t *testing.T) {
]
}
}
HTTP {
SanitizeXForwardedClientCert = true
}
HTTP {
SanitizeXForwardedClientCert = true
Incoming {
RequestNormalization {
InsecureDisablePathNormalization = true
MergeSlashes = true
PathWithEscapedSlashesAction = "UNESCAPE_AND_FORWARD"
HeadersWithUnderscoresAction = "DROP_HEADER"
}
}
}
Peering {
PeerThroughMeshGateways = true
}
@ -2214,6 +2259,7 @@ func TestDecodeConfigEntry(t *testing.T) {
MeshDestinationsOnly: true,
},
AllowEnablingPermissiveMutualTLS: true,
ValidateClusters: true,
TLS: &MeshTLSConfig{
Incoming: &MeshDirectionalTLSConfig{
TLSMinVersion: types.TLSv1_1,
@ -2234,6 +2280,14 @@ func TestDecodeConfigEntry(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: "UNESCAPE_AND_FORWARD",
HeadersWithUnderscoresAction: "DROP_HEADER",
},
},
},
Peering: &PeeringMeshConfig{
PeerThroughMeshGateways: true,

@ -809,6 +809,14 @@ func (o *MeshConfigEntry) DeepCopy() *MeshConfigEntry {
if o.HTTP != nil {
cp.HTTP = new(MeshHTTPConfig)
*cp.HTTP = *o.HTTP
if o.HTTP.Incoming != nil {
cp.HTTP.Incoming = new(MeshDirectionalHTTPConfig)
*cp.HTTP.Incoming = *o.HTTP.Incoming
if o.HTTP.Incoming.RequestNormalization != nil {
cp.HTTP.Incoming.RequestNormalization = new(RequestNormalizationMeshConfig)
*cp.HTTP.Incoming.RequestNormalization = *o.HTTP.Incoming.RequestNormalization
}
}
}
if o.Peering != nil {
cp.Peering = new(PeeringMeshConfig)

@ -1396,6 +1396,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
filterOpts.httpAuthzFilters = append(filterOpts.httpAuthzFilters, addMeta)
}
setNormalizationOptions(cfgSnap.MeshConfig().GetHTTPIncomingRequestNormalization(), &filterOpts)
}
// If an inbound connect limit is set, inject a connection limit filter on each chain.
@ -1464,6 +1465,28 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
return l, err
}
// setNormalizationOptions sets the normalization options for the listener filter.
// This is only used for inbound listeners today (see MeshHTTPConfig).
func setNormalizationOptions(rn *structs.RequestNormalizationMeshConfig, opts *listenerFilterOpts) {
// Note that these options are _always_ set, not just when rn is non-nil. This enables us to set
// Consul defaults (e.g. InsecureDisablePathNormalization = false) that override Envoy defaults
// (e.g. normalize_path = false). We override defaults here rather than in xDS code s.t. Consul
// defaults are only applied where Consul configuration dictates it should be.
opts.normalizePath = !rn.GetInsecureDisablePathNormalization() // invert to enable path normalization by default
opts.mergeSlashes = rn.GetMergeSlashes()
if rn.GetPathWithEscapedSlashesAction() != "" {
v := string(rn.GetPathWithEscapedSlashesAction())
a := envoy_http_v3.HttpConnectionManager_PathWithEscapedSlashesAction_value[v]
opts.pathWithEscapedSlashesAction = envoy_http_v3.HttpConnectionManager_PathWithEscapedSlashesAction(a)
}
if rn.GetHeadersWithUnderscoresAction() != "" {
v := string(rn.GetHeadersWithUnderscoresAction())
a := envoy_core_v3.HttpProtocolOptions_HeadersWithUnderscoresAction_value[v]
opts.headersWithUnderscoresAction = envoy_core_v3.HttpProtocolOptions_HeadersWithUnderscoresAction(a)
}
}
func makePermissiveFilterChain(cfgSnap *proxycfg.ConfigSnapshot, opts listenerFilterOpts) (*envoy_listener_v3.FilterChain, error) {
servicePort := cfgSnap.Proxy.LocalServicePort
if servicePort <= 0 {
@ -2365,16 +2388,20 @@ type listenerFilterOpts struct {
statPrefix string
// HTTP listener filter options
forwardClientDetails bool
forwardClientPolicy envoy_http_v3.HttpConnectionManager_ForwardClientCertDetails
httpAuthzFilters []*envoy_http_v3.HttpFilter
idleTimeoutMs *int
requestTimeoutMs *int
routeName string
routePath string
tracing *envoy_http_v3.HttpConnectionManager_Tracing
useRDS bool
fetchTimeoutRDS *durationpb.Duration
forwardClientDetails bool
forwardClientPolicy envoy_http_v3.HttpConnectionManager_ForwardClientCertDetails
httpAuthzFilters []*envoy_http_v3.HttpFilter
idleTimeoutMs *int
requestTimeoutMs *int
routeName string
routePath string
tracing *envoy_http_v3.HttpConnectionManager_Tracing
normalizePath bool
mergeSlashes bool
pathWithEscapedSlashesAction envoy_http_v3.HttpConnectionManager_PathWithEscapedSlashesAction
headersWithUnderscoresAction envoy_core_v3.HttpProtocolOptions_HeadersWithUnderscoresAction
useRDS bool
fetchTimeoutRDS *durationpb.Duration
}
func makeListenerFilter(opts listenerFilterOpts) (*envoy_listener_v3.Filter, error) {
@ -2490,6 +2517,19 @@ func makeHTTPFilter(opts listenerFilterOpts) (*envoy_listener_v3.Filter, error)
cfg.Tracing = opts.tracing
}
// Request normalization
if opts.normalizePath {
cfg.NormalizePath = &wrapperspb.BoolValue{Value: true}
}
cfg.MergeSlashes = opts.mergeSlashes
cfg.PathWithEscapedSlashesAction = opts.pathWithEscapedSlashesAction
if opts.headersWithUnderscoresAction != 0 { // check for non-default to avoid needless instantiation of options
if cfg.CommonHttpProtocolOptions == nil {
cfg.CommonHttpProtocolOptions = &envoy_core_v3.HttpProtocolOptions{}
}
cfg.CommonHttpProtocolOptions.HeadersWithUnderscoresAction = opts.headersWithUnderscoresAction
}
if opts.useRDS {
if opts.cluster != "" {
return nil, fmt.Errorf("cannot specify cluster name when using RDS")

@ -8,6 +8,8 @@ import (
"testing"
"text/template"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
testinf "github.com/mitchellh/go-testing-interface"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -369,3 +371,71 @@ func TestGetAlpnProtocols(t *testing.T) {
})
}
}
func Test_setNormalizationOptions(t *testing.T) {
tests := map[string]struct {
rn *structs.RequestNormalizationMeshConfig
opts *listenerFilterOpts
want *listenerFilterOpts
}{
"nil entry": {
rn: nil,
opts: &listenerFilterOpts{},
want: &listenerFilterOpts{
normalizePath: true,
},
},
"empty entry": {
rn: &structs.RequestNormalizationMeshConfig{},
opts: &listenerFilterOpts{},
want: &listenerFilterOpts{
normalizePath: true,
},
},
"empty is equivalent to defaults": {
rn: &structs.RequestNormalizationMeshConfig{},
opts: &listenerFilterOpts{},
want: &listenerFilterOpts{
normalizePath: true,
mergeSlashes: false,
pathWithEscapedSlashesAction: envoy_http_v3.HttpConnectionManager_IMPLEMENTATION_SPECIFIC_DEFAULT,
headersWithUnderscoresAction: envoy_core_v3.HttpProtocolOptions_ALLOW,
},
},
"some options": {
rn: &structs.RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: false,
MergeSlashes: true,
PathWithEscapedSlashesAction: "",
HeadersWithUnderscoresAction: "DROP_HEADER",
},
opts: &listenerFilterOpts{},
want: &listenerFilterOpts{
normalizePath: true,
mergeSlashes: true,
headersWithUnderscoresAction: envoy_core_v3.HttpProtocolOptions_DROP_HEADER,
},
},
"all options": {
rn: &structs.RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: true, // note: this is the opposite of the recommended default
MergeSlashes: true,
PathWithEscapedSlashesAction: "REJECT_REQUEST",
HeadersWithUnderscoresAction: "DROP_HEADER",
},
opts: &listenerFilterOpts{},
want: &listenerFilterOpts{
normalizePath: false,
mergeSlashes: true,
pathWithEscapedSlashesAction: envoy_http_v3.HttpConnectionManager_REJECT_REQUEST,
headersWithUnderscoresAction: envoy_core_v3.HttpProtocolOptions_DROP_HEADER,
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
setNormalizationOptions(tc.rn, tc.opts)
assert.Equal(t, tc.want, tc.opts)
})
}
}

@ -1250,7 +1250,7 @@ func convertPermission(perm *structs.IntentionPermission) *envoy_rbac_v3.Permiss
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: hdr.Exact,
},
IgnoreCase: false,
IgnoreCase: hdr.IgnoreCase,
},
}
case hdr.Regex != "":
@ -1259,7 +1259,7 @@ func convertPermission(perm *structs.IntentionPermission) *envoy_rbac_v3.Permiss
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(hdr.Regex),
},
IgnoreCase: false,
// IgnoreCase is not supported for SafeRegex matching per Envoy docs.
},
}
@ -1269,7 +1269,7 @@ func convertPermission(perm *structs.IntentionPermission) *envoy_rbac_v3.Permiss
MatchPattern: &envoy_matcher_v3.StringMatcher_Prefix{
Prefix: hdr.Prefix,
},
IgnoreCase: false,
IgnoreCase: hdr.IgnoreCase,
},
}
@ -1279,7 +1279,17 @@ func convertPermission(perm *structs.IntentionPermission) *envoy_rbac_v3.Permiss
MatchPattern: &envoy_matcher_v3.StringMatcher_Suffix{
Suffix: hdr.Suffix,
},
IgnoreCase: false,
IgnoreCase: hdr.IgnoreCase,
},
}
case hdr.Contains != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Contains{
Contains: hdr.Contains,
},
IgnoreCase: hdr.IgnoreCase,
},
}

@ -786,11 +786,19 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
{Name: "x-bar", Exact: "xyz"},
{Name: "x-dib", Prefix: "gaz"},
{Name: "x-gir", Suffix: "zim"},
{Name: "x-baz", Contains: "qux"},
{Name: "x-zim", Regex: "gi[rR]"},
// Present does not support IgnoreCase
{Name: "y-bar", Exact: "xyz", IgnoreCase: true},
{Name: "y-dib", Prefix: "gaz", IgnoreCase: true},
{Name: "y-gir", Suffix: "zim", IgnoreCase: true},
{Name: "y-baz", Contains: "qux", IgnoreCase: true},
// Regex does not support IgnoreCase
{Name: "z-foo", Present: true, Invert: true},
{Name: "z-bar", Exact: "xyz", Invert: true},
{Name: "z-dib", Prefix: "gaz", Invert: true},
{Name: "z-gir", Suffix: "zim", Invert: true},
{Name: "z-baz", Contains: "qux", Invert: true},
{Name: "z-zim", Regex: "gi[rR]", Invert: true},
},
},
@ -825,15 +833,25 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
Action: structs.IntentionActionDeny,
HTTP: &structs.IntentionHTTPPermission{
Header: []structs.IntentionHTTPHeaderPermission{
// Valid vanilla match options
{Name: "x-foo", Present: true},
{Name: "x-bar", Exact: "xyz"},
{Name: "x-dib", Prefix: "gaz"},
{Name: "x-gir", Suffix: "zim"},
{Name: "x-baz", Contains: "qux"},
{Name: "x-zim", Regex: "gi[rR]"},
// Valid ignore case match options
// (Present and Regex do not support IgnoreCase)
{Name: "y-bar", Exact: "xyz", IgnoreCase: true},
{Name: "y-dib", Prefix: "gaz", IgnoreCase: true},
{Name: "y-gir", Suffix: "zim", IgnoreCase: true},
{Name: "y-baz", Contains: "qux", IgnoreCase: true},
// Valid invert match options
{Name: "z-foo", Present: true, Invert: true},
{Name: "z-bar", Exact: "xyz", Invert: true},
{Name: "z-dib", Prefix: "gaz", Invert: true},
{Name: "z-gir", Suffix: "zim", Invert: true},
{Name: "z-baz", Contains: "qux", Invert: true},
{Name: "z-zim", Regex: "gi[rR]", Invert: true},
},
},

@ -1835,6 +1835,58 @@ func getCustomConfigurationGoldenTestCases(enterprise bool) []goldenTestCase {
}, nil)
},
},
{
// Same as below case, but keeps the recommended default value of InsecureDisablePathNormalization
// to show that the inverse value is reflected in xDS `normalize_path` config.
name: "connect-proxy-with-mesh-config-request-normalization-all-envoy-options-enabled",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
cfgSnap := proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) {
// Ensure public inbound listener has HTTP filter so normalization applies.
ns.Proxy.Config["protocol"] = "http"
// Ensure outbound HTTP listener has HTTP filter so we can observe normalization is not applied.
ns.Proxy.Upstreams[0].Config["protocol"] = "http"
}, nil)
cfgSnap.ConnectProxy.MeshConfig = &structs.MeshConfigEntry{
HTTP: &structs.MeshHTTPConfig{
Incoming: &structs.MeshDirectionalHTTPConfig{
RequestNormalization: &structs.RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: false,
MergeSlashes: true,
PathWithEscapedSlashesAction: "UNESCAPE_AND_FORWARD",
HeadersWithUnderscoresAction: "REJECT_REQUEST",
},
},
},
}
return cfgSnap
},
},
{
// Same as above case, but inverts the recommended default value of InsecureDisablePathNormalization
// to show that the value is respected when explicitly set (does not set `normalize_path`).
name: "connect-proxy-with-mesh-config-request-normalization-all-consul-options",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
cfgSnap := proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) {
// Ensure public inbound listener has HTTP filter so normalization applies.
ns.Proxy.Config["protocol"] = "http"
// Ensure outbound HTTP listener has HTTP filter so we can observe normalization is not applied.
ns.Proxy.Upstreams[0].Config["protocol"] = "http"
}, nil)
cfgSnap.ConnectProxy.MeshConfig = &structs.MeshConfigEntry{
HTTP: &structs.MeshHTTPConfig{
Incoming: &structs.MeshDirectionalHTTPConfig{
RequestNormalization: &structs.RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: true, // note: this is the opposite of the recommended default
MergeSlashes: true,
PathWithEscapedSlashesAction: "UNESCAPE_AND_FORWARD",
HeadersWithUnderscoresAction: "REJECT_REQUEST",
},
},
},
}
return cfgSnap
},
},
}
}

@ -172,6 +172,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -248,4 +249,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -173,6 +173,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -249,4 +250,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -196,6 +196,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -272,4 +273,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -270,6 +270,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -346,4 +347,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -209,6 +209,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -285,4 +286,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -164,6 +164,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -240,4 +241,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -200,6 +200,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -276,4 +277,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -274,6 +274,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -350,4 +351,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -200,6 +200,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -276,4 +277,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -191,6 +191,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -267,4 +268,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -191,6 +191,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -267,4 +268,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -205,6 +205,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -281,4 +282,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -157,6 +157,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -234,4 +235,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -232,4 +233,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -157,6 +157,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -233,4 +234,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -157,6 +157,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -234,4 +235,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -156,6 +156,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -232,4 +233,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -178,6 +178,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -254,4 +255,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -203,6 +203,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -302,4 +303,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -176,6 +176,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -252,4 +253,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -181,6 +181,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -257,4 +258,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -0,0 +1,136 @@
{
"nonce": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"circuitBreakers": {},
"commonLbConfig": {
"healthyPanicThreshold": {}
},
"connectTimeout": "5s",
"edsClusterConfig": {
"edsConfig": {
"ads": {},
"resourceApiVersion": "V3"
}
},
"name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"outlierDetection": {},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"tlsParams": {},
"validationContext": {
"matchTypedSubjectAltNames": [
{
"matcher": {
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db"
},
"sanType": "URI"
}
],
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
}
}
},
"sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
"type": "EDS"
},
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"circuitBreakers": {},
"connectTimeout": "5s",
"edsClusterConfig": {
"edsConfig": {
"ads": {},
"resourceApiVersion": "V3"
}
},
"name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"outlierDetection": {},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"tlsParams": {},
"validationContext": {
"matchTypedSubjectAltNames": [
{
"matcher": {
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target"
},
"sanType": "URI"
},
{
"matcher": {
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target"
},
"sanType": "URI"
}
],
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
}
}
},
"sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul"
}
},
"type": "EDS"
},
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "local_app",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 8080
}
}
}
}
]
}
]
},
"name": "local_app",
"type": "STATIC"
}
],
"typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"versionInfo": "00000001"
}

@ -0,0 +1,136 @@
{
"nonce": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"circuitBreakers": {},
"commonLbConfig": {
"healthyPanicThreshold": {}
},
"connectTimeout": "5s",
"edsClusterConfig": {
"edsConfig": {
"ads": {},
"resourceApiVersion": "V3"
}
},
"name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"outlierDetection": {},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"tlsParams": {},
"validationContext": {
"matchTypedSubjectAltNames": [
{
"matcher": {
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db"
},
"sanType": "URI"
}
],
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
}
}
},
"sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
},
"type": "EDS"
},
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"circuitBreakers": {},
"connectTimeout": "5s",
"edsClusterConfig": {
"edsConfig": {
"ads": {},
"resourceApiVersion": "V3"
}
},
"name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"outlierDetection": {},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"tlsParams": {},
"validationContext": {
"matchTypedSubjectAltNames": [
{
"matcher": {
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target"
},
"sanType": "URI"
},
{
"matcher": {
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target"
},
"sanType": "URI"
}
],
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
}
}
},
"sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul"
}
},
"type": "EDS"
},
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "local_app",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 8080
}
}
}
}
]
}
]
},
"name": "local_app",
"type": "STATIC"
}
],
"typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"versionInfo": "00000001"
}

@ -0,0 +1,75 @@
{
"nonce": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.1",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
},
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.2",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
}
]
}
]
},
{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.1",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
},
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.20.1.2",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
}
]
}
]
}
],
"typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"versionInfo": "00000001"
}

@ -0,0 +1,75 @@
{
"nonce": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.1",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
},
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.2",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
}
]
}
]
},
{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.1",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
},
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.20.1.2",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
}
]
}
]
}
],
"typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"versionInfo": "00000001"
}

@ -0,0 +1,275 @@
{
"nonce": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 9191
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"httpFilters": [
{
"name": "envoy.filters.http.router",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
}
}
],
"routeConfig": {
"name": "db",
"virtualHosts": [
{
"domains": [
"*"
],
"name": "db.default.default.dc1",
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
]
},
"statPrefix": "upstream.db.default.default.dc1",
"tracing": {
"randomSampling": {}
},
"upgradeConfigs": [
{
"upgradeType": "websocket"
}
]
}
}
]
}
],
"name": "db:127.0.0.1:9191",
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"address": {
"socketAddress": {
"address": "127.10.10.10",
"portValue": 8181
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"statPrefix": "upstream.prepared_query_geo-cache"
}
}
]
}
],
"name": "prepared_query:geo-cache:127.10.10.10:8181",
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 9999
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"commonHttpProtocolOptions": {
"headersWithUnderscoresAction": "REJECT_REQUEST"
},
"forwardClientCertDetails": "APPEND_FORWARD",
"httpFilters": [
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {}
}
},
{
"name": "envoy.filters.http.header_to_metadata",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config",
"requestRules": [
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "trust-domain",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\1"
}
}
},
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "partition",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\2"
}
}
},
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "namespace",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\3"
}
}
},
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "datacenter",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\4"
}
}
},
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "service",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\5"
}
}
}
]
}
},
{
"name": "envoy.filters.http.router",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
}
}
],
"mergeSlashes": true,
"pathWithEscapedSlashesAction": "UNESCAPE_AND_FORWARD",
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
{
"domains": [
"*"
],
"name": "public_listener",
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "local_app"
}
}
]
}
]
},
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "public_listener",
"tracing": {
"randomSampling": {}
},
"upgradeConfigs": [
{
"upgradeType": "websocket"
}
]
}
}
],
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
"commonTlsContext": {
"alpnProtocols": [
"http/1.1"
],
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"tlsParams": {},
"validationContext": {
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
}
}
},
"requireClientCertificate": true
}
}
}
],
"name": "public_listener:0.0.0.0:9999",
"trafficDirection": "INBOUND"
}
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}

@ -0,0 +1,276 @@
{
"nonce": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 9191
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"httpFilters": [
{
"name": "envoy.filters.http.router",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
}
}
],
"routeConfig": {
"name": "db",
"virtualHosts": [
{
"domains": [
"*"
],
"name": "db.default.default.dc1",
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
]
},
"statPrefix": "upstream.db.default.default.dc1",
"tracing": {
"randomSampling": {}
},
"upgradeConfigs": [
{
"upgradeType": "websocket"
}
]
}
}
]
}
],
"name": "db:127.0.0.1:9191",
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"address": {
"socketAddress": {
"address": "127.10.10.10",
"portValue": 8181
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"statPrefix": "upstream.prepared_query_geo-cache"
}
}
]
}
],
"name": "prepared_query:geo-cache:127.10.10.10:8181",
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 9999
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"commonHttpProtocolOptions": {
"headersWithUnderscoresAction": "REJECT_REQUEST"
},
"forwardClientCertDetails": "APPEND_FORWARD",
"httpFilters": [
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {}
}
},
{
"name": "envoy.filters.http.header_to_metadata",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config",
"requestRules": [
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "trust-domain",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\1"
}
}
},
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "partition",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\2"
}
}
},
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "namespace",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\3"
}
}
},
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "datacenter",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\4"
}
}
},
{
"header": "x-forwarded-client-cert",
"onHeaderPresent": {
"key": "service",
"metadataNamespace": "consul",
"regexValueRewrite": {
"pattern": {
"regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*"
},
"substitution": "\\5"
}
}
}
]
}
},
{
"name": "envoy.filters.http.router",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
}
}
],
"mergeSlashes": true,
"normalizePath": true,
"pathWithEscapedSlashesAction": "UNESCAPE_AND_FORWARD",
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
{
"domains": [
"*"
],
"name": "public_listener",
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "local_app"
}
}
]
}
]
},
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "public_listener",
"tracing": {
"randomSampling": {}
},
"upgradeConfigs": [
{
"upgradeType": "websocket"
}
]
}
}
],
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
"commonTlsContext": {
"alpnProtocols": [
"http/1.1"
],
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"tlsParams": {},
"validationContext": {
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
}
}
},
"requireClientCertificate": true
}
}
}
],
"name": "public_listener:0.0.0.0:9999",
"trafficDirection": "INBOUND"
}
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -254,4 +255,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -106,6 +106,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -176,4 +177,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -95,6 +95,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -165,4 +166,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -233,4 +234,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -81,6 +81,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -150,4 +151,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -155,6 +155,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -231,4 +232,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -156,6 +156,7 @@
}
}
],
"normalizePath": true,
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
@ -233,4 +234,4 @@
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"versionInfo": "00000001"
}
}

@ -114,6 +114,14 @@
}
}
},
{
"header": {
"name": "x-baz",
"stringMatch": {
"contains": "qux"
}
}
},
{
"header": {
"name": "x-zim",
@ -124,6 +132,42 @@
}
}
},
{
"header": {
"name": "y-bar",
"stringMatch": {
"exact": "xyz",
"ignoreCase": true
}
}
},
{
"header": {
"name": "y-dib",
"stringMatch": {
"ignoreCase": true,
"prefix": "gaz"
}
}
},
{
"header": {
"name": "y-gir",
"stringMatch": {
"ignoreCase": true,
"suffix": "zim"
}
}
},
{
"header": {
"name": "y-baz",
"stringMatch": {
"contains": "qux",
"ignoreCase": true
}
}
},
{
"header": {
"invertMatch": true,
@ -158,6 +202,15 @@
}
}
},
{
"header": {
"invertMatch": true,
"name": "z-baz",
"stringMatch": {
"contains": "qux"
}
}
},
{
"header": {
"invertMatch": true,
@ -236,4 +289,4 @@
}
}
}
}
}

@ -113,6 +113,14 @@
}
}
},
{
"header": {
"name": "x-baz",
"stringMatch": {
"contains": "qux"
}
}
},
{
"header": {
"name": "x-zim",
@ -123,6 +131,42 @@
}
}
},
{
"header": {
"name": "y-bar",
"stringMatch": {
"exact": "xyz",
"ignoreCase": true
}
}
},
{
"header": {
"name": "y-dib",
"stringMatch": {
"ignoreCase": true,
"prefix": "gaz"
}
}
},
{
"header": {
"name": "y-gir",
"stringMatch": {
"ignoreCase": true,
"suffix": "zim"
}
}
},
{
"header": {
"name": "y-baz",
"stringMatch": {
"contains": "qux",
"ignoreCase": true
}
}
},
{
"header": {
"invertMatch": true,
@ -157,6 +201,15 @@
}
}
},
{
"header": {
"invertMatch": true,
"name": "z-baz",
"stringMatch": {
"contains": "qux"
}
}
},
{
"header": {
"invertMatch": true,
@ -235,4 +288,4 @@
}
}
}
}
}

@ -0,0 +1,5 @@
{
"nonce": "00000001",
"typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"versionInfo": "00000001"
}

@ -0,0 +1,5 @@
{
"nonce": "00000001",
"typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"versionInfo": "00000001"
}

@ -0,0 +1,5 @@
{
"nonce": "00000001",
"typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
"versionInfo": "00000001"
}

@ -0,0 +1,5 @@
{
"nonce": "00000001",
"typeUrl": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
"versionInfo": "00000001"
}

@ -63,13 +63,15 @@ type IntentionHTTPPermission struct {
}
type IntentionHTTPHeaderPermission struct {
Name string
Present bool `json:",omitempty"`
Exact string `json:",omitempty"`
Prefix string `json:",omitempty"`
Suffix string `json:",omitempty"`
Regex string `json:",omitempty"`
Invert bool `json:",omitempty"`
Name string
Present bool `json:",omitempty"`
Exact string `json:",omitempty"`
Prefix string `json:",omitempty"`
Suffix string `json:",omitempty"`
Contains string `json:",omitempty"`
Regex string `json:",omitempty"`
Invert bool `json:",omitempty"`
IgnoreCase bool `json:",omitempty" alias:"ignore_case"`
}
type IntentionJWTRequirement struct {

@ -69,12 +69,53 @@ type MeshDirectionalTLSConfig struct {
type MeshHTTPConfig struct {
SanitizeXForwardedClientCert bool `alias:"sanitize_x_forwarded_client_cert"`
// Incoming configures settings for incoming HTTP traffic to mesh proxies.
Incoming *MeshDirectionalHTTPConfig `json:",omitempty"`
}
// MeshDirectionalHTTPConfig holds mesh configuration specific to HTTP
// requests for a given traffic direction.
type MeshDirectionalHTTPConfig struct {
RequestNormalization *RequestNormalizationMeshConfig `json:",omitempty" alias:"request_normalization"`
}
type PeeringMeshConfig struct {
PeerThroughMeshGateways bool `json:",omitempty" alias:"peer_through_mesh_gateways"`
}
// 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`. The default value is \`false\`. When set to \`true\` in Consul, \`normalize_path\` is
// set to \`false\` for the Envoy proxy. This parameter disables the normalization of request URL paths according to
// RFC 3986, conversion of \`\\\` to \`/\`, and decoding non-reserved %-encoded characters. When using L7 intentions
// with path match rules, we recommend enabling path normalization in order to avoid match rule circumvention with
// non-normalized path values.
InsecureDisablePathNormalization bool `json:",omitempty" alias:"insecure_disable_path_normalization"`
// MergeSlashes sets the value of the \`merge_slashes\` option in the Envoy listener's \`HttpConnectionManager\`.
// The default value is \`false\`. This option controls the normalization of request URL paths by merging
// consecutive \`/\` characters. This normalization is not part of RFC 3986. When using L7 intentions with path
// match rules, we recommend enabling this setting to avoid match rule circumvention through 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.
MergeSlashes bool `json:",omitempty" alias:"merge_slashes"`
// PathWithEscapedSlashesAction sets the value of the \`path_with_escaped_slashes_action\` option in the Envoy
// listener's \`HttpConnectionManager\`. The default value of this option is empty, which is equivalent to
// \`IMPLEMENTATION_SPECIFIC_DEFAULT\`. This parameter controls the action taken in response to request URL paths
// with escaped slashes in the path. When using L7 intentions with path match rules, we recommend enabling this
// setting to avoid match rule circumvention through 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. Refer to the Envoy documentation for more information on available
// options.
PathWithEscapedSlashesAction string `json:",omitempty" alias:"path_with_escaped_slashes_action"`
// 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\`. Refer to the Envoy documentation for more information on available
// options.
HeadersWithUnderscoresAction string `json:",omitempty" alias:"headers_with_underscores_action"`
}
func (e *MeshConfigEntry) GetKind() string { return MeshConfig }
func (e *MeshConfigEntry) GetName() string { return MeshConfigMesh }
func (e *MeshConfigEntry) GetPartition() string { return e.Partition }

@ -2305,6 +2305,10 @@ func TestParseConfigEntry(t *testing.T) {
name = "hdr-suffix"
suffix = "suffix"
},
{
name = "hdr-contains"
contains = "contains"
},
{
name = "hdr-regex"
regex = "regex"
@ -2313,7 +2317,12 @@ func TestParseConfigEntry(t *testing.T) {
name = "hdr-absent"
present = true
invert = true
}
},
{
name = "hdr-ignore-case"
exact = "exact"
ignore_case = true
},
]
}
},
@ -2382,6 +2391,10 @@ func TestParseConfigEntry(t *testing.T) {
Name = "hdr-suffix"
Suffix = "suffix"
},
{
Name = "hdr-contains"
Contains = "contains"
},
{
Name = "hdr-regex"
Regex = "regex"
@ -2390,6 +2403,11 @@ func TestParseConfigEntry(t *testing.T) {
Name = "hdr-absent"
Present = true
Invert = true
},
{
Name = "hdr-ignore-case"
Exact = "exact"
IgnoreCase = true
}
]
}
@ -2460,6 +2478,10 @@ func TestParseConfigEntry(t *testing.T) {
"name": "hdr-suffix",
"suffix": "suffix"
},
{
"name": "hdr-contains",
"contains": "contains"
},
{
"name": "hdr-regex",
"regex": "regex"
@ -2468,6 +2490,11 @@ func TestParseConfigEntry(t *testing.T) {
"name": "hdr-absent",
"present": true,
"invert": true
},
{
"name": "hdr-ignore-case",
"exact": "exact",
"ignore_case": true
}
]
}
@ -2542,6 +2569,10 @@ func TestParseConfigEntry(t *testing.T) {
"Name": "hdr-suffix",
"Suffix": "suffix"
},
{
"Name": "hdr-contains",
"Contains": "contains"
},
{
"Name": "hdr-regex",
"Regex": "regex"
@ -2550,6 +2581,11 @@ func TestParseConfigEntry(t *testing.T) {
"Name": "hdr-absent",
"Present": true,
"Invert": true
},
{
"Name": "hdr-ignore-case",
"Exact": "exact",
"IgnoreCase": true
}
]
}
@ -2623,6 +2659,10 @@ func TestParseConfigEntry(t *testing.T) {
Name: "hdr-suffix",
Suffix: "suffix",
},
{
Name: "hdr-contains",
Contains: "contains",
},
{
Name: "hdr-regex",
Regex: "regex",
@ -2632,6 +2672,11 @@ func TestParseConfigEntry(t *testing.T) {
Present: true,
Invert: true,
},
{
Name: "hdr-ignore-case",
Exact: "exact",
IgnoreCase: true,
},
},
},
},
@ -2719,7 +2764,7 @@ func TestParseConfigEntry(t *testing.T) {
},
},
{
name: "mesh",
name: "mesh: kitchen sink",
snake: `
kind = "mesh"
meta {
@ -2729,6 +2774,8 @@ func TestParseConfigEntry(t *testing.T) {
transparent_proxy {
mesh_destinations_only = true
}
allow_enabling_permissive_mutual_tls = true
validate_clusters = true
tls {
incoming {
tls_min_version = "TLSv1_1"
@ -2747,6 +2794,20 @@ func TestParseConfigEntry(t *testing.T) {
]
}
}
http {
sanitize_x_forwarded_client_cert = true
incoming {
request_normalization {
insecure_disable_path_normalization = true
merge_slashes = true
path_with_escaped_slashes_action = "UNESCAPE_AND_FORWARD"
headers_with_underscores_action = "DROP_HEADER"
}
}
}
peering {
peer_through_mesh_gateways = true
}
`,
camel: `
Kind = "mesh"
@ -2757,6 +2818,8 @@ func TestParseConfigEntry(t *testing.T) {
TransparentProxy {
MeshDestinationsOnly = true
}
AllowEnablingPermissiveMutualTLS = true
ValidateClusters = true
TLS {
Incoming {
TLSMinVersion = "TLSv1_1"
@ -2775,6 +2838,20 @@ func TestParseConfigEntry(t *testing.T) {
]
}
}
HTTP {
SanitizeXForwardedClientCert = true
Incoming {
RequestNormalization {
InsecureDisablePathNormalization = true
MergeSlashes = true
PathWithEscapedSlashesAction = "UNESCAPE_AND_FORWARD"
HeadersWithUnderscoresAction = "DROP_HEADER"
}
}
}
Peering {
PeerThroughMeshGateways = true
}
`,
snakeJSON: `
{
@ -2786,6 +2863,8 @@ func TestParseConfigEntry(t *testing.T) {
"transparent_proxy": {
"mesh_destinations_only": true
},
"allow_enabling_permissive_mutual_tls": true,
"validate_clusters": true,
"tls": {
"incoming": {
"tls_min_version": "TLSv1_1",
@ -2803,6 +2882,20 @@ func TestParseConfigEntry(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
]
}
},
"http": {
"sanitize_x_forwarded_client_cert": true,
"incoming": {
"request_normalization": {
"insecure_disable_path_normalization": true,
"merge_slashes": true,
"path_with_escaped_slashes_action": "UNESCAPE_AND_FORWARD",
"headers_with_underscores_action": "DROP_HEADER"
}
}
},
"peering": {
"peer_through_mesh_gateways": true
}
}
`,
@ -2816,6 +2909,8 @@ func TestParseConfigEntry(t *testing.T) {
"TransparentProxy": {
"MeshDestinationsOnly": true
},
"AllowEnablingPermissiveMutualTLS": true,
"ValidateClusters": true,
"TLS": {
"Incoming": {
"TLSMinVersion": "TLSv1_1",
@ -2833,6 +2928,20 @@ func TestParseConfigEntry(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
]
}
},
"HTTP": {
"SanitizeXForwardedClientCert": true,
"Incoming": {
"RequestNormalization": {
"InsecureDisablePathNormalization": true,
"MergeSlashes": true,
"PathWithEscapedSlashesAction": "UNESCAPE_AND_FORWARD",
"HeadersWithUnderscoresAction": "DROP_HEADER"
}
}
},
"Peering": {
"PeerThroughMeshGateways": true
}
}
`,
@ -2844,6 +2953,8 @@ func TestParseConfigEntry(t *testing.T) {
TransparentProxy: api.TransparentProxyMeshConfig{
MeshDestinationsOnly: true,
},
AllowEnablingPermissiveMutualTLS: true,
ValidateClusters: true,
TLS: &api.MeshTLSConfig{
Incoming: &api.MeshDirectionalTLSConfig{
TLSMinVersion: "TLSv1_1",
@ -2862,6 +2973,20 @@ func TestParseConfigEntry(t *testing.T) {
},
},
},
HTTP: &api.MeshHTTPConfig{
SanitizeXForwardedClientCert: true,
Incoming: &api.MeshDirectionalHTTPConfig{
RequestNormalization: &api.RequestNormalizationMeshConfig{
InsecureDisablePathNormalization: true,
MergeSlashes: true,
PathWithEscapedSlashesAction: "UNESCAPE_AND_FORWARD",
HeadersWithUnderscoresAction: "DROP_HEADER",
},
},
},
Peering: &api.PeeringMeshConfig{
PeerThroughMeshGateways: true,
},
},
},
{

@ -1153,8 +1153,10 @@ func IntentionHTTPHeaderPermissionToStructs(s *IntentionHTTPHeaderPermission, t
t.Exact = s.Exact
t.Prefix = s.Prefix
t.Suffix = s.Suffix
t.Contains = s.Contains
t.Regex = s.Regex
t.Invert = s.Invert
t.IgnoreCase = s.IgnoreCase
}
func IntentionHTTPHeaderPermissionFromStructs(t *structs.IntentionHTTPHeaderPermission, s *IntentionHTTPHeaderPermission) {
if s == nil {
@ -1165,8 +1167,10 @@ func IntentionHTTPHeaderPermissionFromStructs(t *structs.IntentionHTTPHeaderPerm
s.Exact = t.Exact
s.Prefix = t.Prefix
s.Suffix = t.Suffix
s.Contains = t.Contains
s.Regex = t.Regex
s.Invert = t.Invert
s.IgnoreCase = t.IgnoreCase
}
func IntentionHTTPPermissionToStructs(s *IntentionHTTPPermission, t *structs.IntentionHTTPPermission) {
if s == nil {
@ -1758,6 +1762,26 @@ func MeshConfigFromStructs(t *structs.MeshConfigEntry, s *MeshConfig) {
s.Meta = t.Meta
s.Hash = t.Hash
}
func MeshDirectionalHTTPConfigToStructs(s *MeshDirectionalHTTPConfig, t *structs.MeshDirectionalHTTPConfig) {
if s == nil {
return
}
if s.RequestNormalization != nil {
var x structs.RequestNormalizationMeshConfig
RequestNormalizationMeshConfigToStructs(s.RequestNormalization, &x)
t.RequestNormalization = &x
}
}
func MeshDirectionalHTTPConfigFromStructs(t *structs.MeshDirectionalHTTPConfig, s *MeshDirectionalHTTPConfig) {
if s == nil {
return
}
if t.RequestNormalization != nil {
var x RequestNormalizationMeshConfig
RequestNormalizationMeshConfigFromStructs(t.RequestNormalization, &x)
s.RequestNormalization = &x
}
}
func MeshDirectionalTLSConfigToStructs(s *MeshDirectionalTLSConfig, t *structs.MeshDirectionalTLSConfig) {
if s == nil {
return
@ -1791,12 +1815,22 @@ func MeshHTTPConfigToStructs(s *MeshHTTPConfig, t *structs.MeshHTTPConfig) {
return
}
t.SanitizeXForwardedClientCert = s.SanitizeXForwardedClientCert
if s.Incoming != nil {
var x structs.MeshDirectionalHTTPConfig
MeshDirectionalHTTPConfigToStructs(s.Incoming, &x)
t.Incoming = &x
}
}
func MeshHTTPConfigFromStructs(t *structs.MeshHTTPConfig, s *MeshHTTPConfig) {
if s == nil {
return
}
s.SanitizeXForwardedClientCert = t.SanitizeXForwardedClientCert
if t.Incoming != nil {
var x MeshDirectionalHTTPConfig
MeshDirectionalHTTPConfigFromStructs(t.Incoming, &x)
s.Incoming = &x
}
}
func MeshTLSConfigToStructs(s *MeshTLSConfig, t *structs.MeshTLSConfig) {
if s == nil {
@ -1916,6 +1950,24 @@ func RemoteJWKSFromStructs(t *structs.RemoteJWKS, s *RemoteJWKS) {
s.JWKSCluster = &x
}
}
func RequestNormalizationMeshConfigToStructs(s *RequestNormalizationMeshConfig, t *structs.RequestNormalizationMeshConfig) {
if s == nil {
return
}
t.InsecureDisablePathNormalization = s.InsecureDisablePathNormalization
t.MergeSlashes = s.MergeSlashes
t.PathWithEscapedSlashesAction = pathWithEscapedSlashesActionToStructs(s.PathWithEscapedSlashesAction)
t.HeadersWithUnderscoresAction = headersWithUnderscoresActionToStructs(s.HeadersWithUnderscoresAction)
}
func RequestNormalizationMeshConfigFromStructs(t *structs.RequestNormalizationMeshConfig, s *RequestNormalizationMeshConfig) {
if s == nil {
return
}
s.InsecureDisablePathNormalization = t.InsecureDisablePathNormalization
s.MergeSlashes = t.MergeSlashes
s.PathWithEscapedSlashesAction = pathWithEscapedSlashesActionFromStructs(t.PathWithEscapedSlashesAction)
s.HeadersWithUnderscoresAction = headersWithUnderscoresActionFromStructs(t.HeadersWithUnderscoresAction)
}
func ResourceReferenceToStructs(s *ResourceReference, t *structs.ResourceReference) {
if s == nil {
return

@ -291,6 +291,66 @@ func cipherSuitesFromStructs(cs []types.TLSCipherSuite) []string {
return cipherSuites
}
func pathWithEscapedSlashesActionFromStructs(a structs.PathWithEscapedSlashesAction) PathWithEscapedSlashesAction {
switch a {
case structs.PathWithEscapedSlashesActionDefault:
return PathWithEscapedSlashesAction_PathWithEscapedSlashesActionDefault
case structs.PathWithEscapedSlashesActionKeep:
return PathWithEscapedSlashesAction_PathWithEscapedSlashesActionKeep
case structs.PathWithEscapedSlashesActionReject:
return PathWithEscapedSlashesAction_PathWithEscapedSlashesActionReject
case structs.PathWithEscapedSlashesActionUnescapeAndRedirect:
return PathWithEscapedSlashesAction_PathWithEscapedSlashesActionUnescapeAndRedirect
case structs.PathWithEscapedSlashesActionUnescapeAndForward:
return PathWithEscapedSlashesAction_PathWithEscapedSlashesActionUnescapeAndForward
default:
return PathWithEscapedSlashesAction_PathWithEscapedSlashesActionDefault
}
}
func pathWithEscapedSlashesActionToStructs(a PathWithEscapedSlashesAction) structs.PathWithEscapedSlashesAction {
switch a {
case PathWithEscapedSlashesAction_PathWithEscapedSlashesActionDefault:
return structs.PathWithEscapedSlashesActionDefault
case PathWithEscapedSlashesAction_PathWithEscapedSlashesActionKeep:
return structs.PathWithEscapedSlashesActionKeep
case PathWithEscapedSlashesAction_PathWithEscapedSlashesActionReject:
return structs.PathWithEscapedSlashesActionReject
case PathWithEscapedSlashesAction_PathWithEscapedSlashesActionUnescapeAndRedirect:
return structs.PathWithEscapedSlashesActionUnescapeAndRedirect
case PathWithEscapedSlashesAction_PathWithEscapedSlashesActionUnescapeAndForward:
return structs.PathWithEscapedSlashesActionUnescapeAndForward
default:
return structs.PathWithEscapedSlashesActionDefault
}
}
func headersWithUnderscoresActionFromStructs(a structs.HeadersWithUnderscoresAction) HeadersWithUnderscoresAction {
switch a {
case structs.HeadersWithUnderscoresActionAllow:
return HeadersWithUnderscoresAction_HeadersWithUnderscoresActionAllow
case structs.HeadersWithUnderscoresActionRejectRequest:
return HeadersWithUnderscoresAction_HeadersWithUnderscoresActionRejectRequest
case structs.HeadersWithUnderscoresActionDropHeader:
return HeadersWithUnderscoresAction_HeadersWithUnderscoresActionDropHeader
default:
return HeadersWithUnderscoresAction_HeadersWithUnderscoresActionAllow
}
}
func headersWithUnderscoresActionToStructs(a HeadersWithUnderscoresAction) structs.HeadersWithUnderscoresAction {
switch a {
case HeadersWithUnderscoresAction_HeadersWithUnderscoresActionAllow:
return structs.HeadersWithUnderscoresActionAllow
case HeadersWithUnderscoresAction_HeadersWithUnderscoresActionRejectRequest:
return structs.HeadersWithUnderscoresActionRejectRequest
case HeadersWithUnderscoresAction_HeadersWithUnderscoresActionDropHeader:
return structs.HeadersWithUnderscoresActionDropHeader
default:
return structs.HeadersWithUnderscoresActionAllow
}
}
func enterpriseMetaToStructs(m *pbcommon.EnterpriseMeta) acl.EnterpriseMeta {
var entMeta acl.EnterpriseMeta
pbcommon.EnterpriseMetaToStructs(m, &entMeta)

@ -107,6 +107,16 @@ func (msg *MeshHTTPConfig) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *MeshDirectionalHTTPConfig) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *MeshDirectionalHTTPConfig) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *PeeringMeshConfig) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
@ -117,6 +127,16 @@ func (msg *PeeringMeshConfig) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *RequestNormalizationMeshConfig) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *RequestNormalizationMeshConfig) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *ServiceResolver) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)

File diff suppressed because it is too large Load Diff

@ -139,6 +139,16 @@ message MeshDirectionalTLSConfig {
// name=Structs
message MeshHTTPConfig {
bool SanitizeXForwardedClientCert = 1;
MeshDirectionalHTTPConfig Incoming = 2;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.MeshDirectionalHTTPConfig
// output=config_entry.gen.go
// name=Structs
message MeshDirectionalHTTPConfig {
RequestNormalizationMeshConfig RequestNormalization = 1;
}
// mog annotation:
@ -150,6 +160,34 @@ message PeeringMeshConfig {
bool PeerThroughMeshGateways = 1;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.RequestNormalizationMeshConfig
// output=config_entry.gen.go
// name=Structs
message RequestNormalizationMeshConfig {
bool InsecureDisablePathNormalization = 1;
bool MergeSlashes = 2;
// mog: func-to=pathWithEscapedSlashesActionToStructs func-from=pathWithEscapedSlashesActionFromStructs
PathWithEscapedSlashesAction PathWithEscapedSlashesAction = 3;
// mog: func-to=headersWithUnderscoresActionToStructs func-from=headersWithUnderscoresActionFromStructs
HeadersWithUnderscoresAction HeadersWithUnderscoresAction = 4;
}
enum PathWithEscapedSlashesAction {
PathWithEscapedSlashesActionDefault = 0;
PathWithEscapedSlashesActionKeep = 1;
PathWithEscapedSlashesActionReject = 2;
PathWithEscapedSlashesActionUnescapeAndRedirect = 3;
PathWithEscapedSlashesActionUnescapeAndForward = 4;
}
enum HeadersWithUnderscoresAction {
HeadersWithUnderscoresActionAllow = 0;
HeadersWithUnderscoresActionRejectRequest = 1;
HeadersWithUnderscoresActionDropHeader = 2;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.ServiceResolverConfigEntry
@ -521,6 +559,8 @@ message IntentionHTTPHeaderPermission {
string Suffix = 5;
string Regex = 6;
bool Invert = 7;
string Contains = 8;
bool IgnoreCase = 9;
}
// mog annotation:

@ -0,0 +1,5 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
// Use default-allow policy so that we can test specific deny rules
default_intention_policy = "allow"

@ -0,0 +1,7 @@
#!/bin/bash
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
snapshot_envoy_admin localhost:19000 s1 primary || true
snapshot_envoy_admin localhost:19001 s2 || true

@ -0,0 +1,101 @@
#!/bin/bash
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
set -euo pipefail
upsert_config_entry primary '
kind = "service-defaults"
name = "s2"
protocol = "http"
'
upsert_config_entry primary '
kind = "mesh"
http {
incoming {
request_normalization {
insecure_disable_path_normalization = true
merge_slashes = false // explicitly set to the default for clarity
path_with_escaped_slashes_action = "" // explicitly set to the default for clarity
headers_with_underscores_action = "" // explicitly set to the default for clarity
}
}
}
'
upsert_config_entry primary '
kind = "service-intentions"
name = "s2"
sources {
name = "s1"
permissions = [
// paths
{
action = "deny"
http {
path_exact = "/value/supersecret"
}
},
// headers
{
action = "deny"
http {
header = [{
name = "x-check"
contains = "bad"
ignore_case = true
}]
}
},
{
action = "deny"
http {
header = [{
name = "x-check"
exact = "exactbad"
ignore_case = true
}]
}
},
{
action = "deny"
http {
header = [{
name = "x-check"
prefix = "prebad-"
ignore_case = true
}]
}
},
{
action = "deny"
http {
header = [{
name = "x-check"
suffix = "-sufbad"
ignore_case = true
}]
}
},
// redundant with above case, but included for real-world example
// and to cover values containing ".".
{
action = "deny"
http {
header = [{
name = "Host"
suffix = "bad.com"
ignore_case = true
}]
}
}
]
}
'
register_services primary
gen_envoy_bootstrap s1 19000
gen_envoy_bootstrap s2 19001

@ -0,0 +1,6 @@
#!/bin/bash
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
# Allow for non-normalized path testing by using alternative container.
export SERVICE_CONTAINER="echo"

@ -0,0 +1,99 @@
#!/usr/bin/env bats
load helpers
@test "s1 proxy admin is up on :19000" {
retry_default curl -f -s localhost:19000/stats -o /dev/null
}
@test "s2 proxy admin is up on :19001" {
retry_default curl -f -s localhost:19001/stats -o /dev/null
}
@test "s1 proxy listener should be up and have right cert" {
assert_proxy_presents_cert_uri localhost:21000 s1
}
@test "s2 proxy listener should be up and have right cert" {
assert_proxy_presents_cert_uri localhost:21001 s2
}
@test "s2 proxies should be healthy" {
assert_service_has_healthy_instances s2 1
}
@test "s1 upstream should have healthy endpoints for s2" {
assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 1
}
@test "s2 should have http rbac rules loaded from xDS" {
retry_default assert_envoy_http_rbac_policy_count localhost:19001 1
}
# The following tests exercise the same cases in "case-l7-intentions-request-normalization"
# but with all normalization disabled, including default path normalization. Note that
# disabling normalization is not recommended in production environments unless specifically
# required.
@test "test allowed path" {
retry_default must_pass_http_request GET localhost:5000/foo
retry_default must_pass_http_request GET localhost:5000/value/foo
retry_default must_pass_http_request GET localhost:5000/foo/supersecret
}
@test "test disallowed path" {
retry_default must_fail_http_request 403 GET 'localhost:5000/value/supersecret'
retry_default must_fail_http_request 403 GET 'localhost:5000/value/supersecret#foo'
retry_default must_fail_http_request 403 GET 'localhost:5000/value/supersecret?'
}
@test "test ignored disallowed path with repeat slashes" {
retry_default must_pass_http_request GET 'localhost:5000/value//supersecret'
get_echo_request_path | grep -Fx '/value//supersecret'
retry_default must_pass_http_request GET 'localhost:5000/value///supersecret'
get_echo_request_path | grep -Fx '/value///supersecret'
}
@test "test ignored disallowed path with escaped characters" {
# escaped '/' (HTTP reserved)
retry_default must_pass_http_request GET 'localhost:5000/value%2Fsupersecret'
get_echo_request_path | grep -Fx '/value%2Fsupersecret'
# escaped 'v' (not HTTP reserved)
retry_default must_pass_http_request GET 'localhost:5000/value/%73upersecret'
get_echo_request_path | grep -Fx '/value/%73upersecret'
}
@test "test ignored disallowed path with backward slashes" {
# URLs must be quoted due to backslashes, otherwise shell erases them
retry_default must_pass_http_request GET 'localhost:5000/value\supersecret'
get_echo_request_path | grep -Fx '/value\supersecret'
retry_default must_pass_http_request GET 'localhost:5000/value\\supersecret'
get_echo_request_path | grep -Fx '/value\\supersecret'
retry_default must_pass_http_request GET 'localhost:5000/value\/supersecret'
get_echo_request_path | grep -Fx '/value\/supersecret'
retry_default must_pass_http_request GET 'localhost:5000/value/\/supersecret'
get_echo_request_path | grep -Fx '/value/\/supersecret'
}
@test "test ignored underscore in header key" {
retry_default must_pass_http_request GET localhost:5000/foo x_poison:anything
get_echo_request_header_value "x_poison" | grep -Fx 'anything'
retry_default must_pass_http_request GET localhost:5000/foo x_check:bad
get_echo_request_header_value "x_check" | grep -Fx 'bad'
retry_default must_pass_http_request GET localhost:5000/foo x_check:good-sufbad
get_echo_request_header_value "x_check" | grep -Fx 'good-sufbad'
retry_default must_pass_http_request GET localhost:5000/foo x_check:prebad-good
get_echo_request_header_value "x_check" | grep -Fx 'prebad-good'
}
# Header contains and ignoreCase are not expected to change behavior with normalization
# disabled, so those cases from "case-l7-intentions-request-normalization" are omitted here.
# @test "s1 upstream should NOT be able to connect to s2" {
# run retry_default must_fail_tcp_connection localhost:5000
# echo "OUTPUT $output"
# [ "$status" == "0" ]
# }

@ -0,0 +1,5 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
// Use default-allow policy so that we can test specific deny rules
default_intention_policy = "allow"

@ -0,0 +1,7 @@
#!/bin/bash
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
snapshot_envoy_admin localhost:19000 s1 primary || true
snapshot_envoy_admin localhost:19001 s2 || true

@ -0,0 +1,101 @@
#!/bin/bash
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
set -euo pipefail
upsert_config_entry primary '
kind = "service-defaults"
name = "s2"
protocol = "http"
'
upsert_config_entry primary '
kind = "mesh"
http {
incoming {
request_normalization {
insecure_disable_path_normalization = false // explicitly set to the default for clarity
merge_slashes = true
path_with_escaped_slashes_action = "UNESCAPE_AND_FORWARD"
headers_with_underscores_action = "REJECT_REQUEST"
}
}
}
'
upsert_config_entry primary '
kind = "service-intentions"
name = "s2"
sources {
name = "s1"
permissions = [
// paths
{
action = "deny"
http {
path_exact = "/value/supersecret"
}
},
// headers
{
action = "deny"
http {
header = [{
name = "x-check"
contains = "bad"
ignore_case = true
}]
}
},
{
action = "deny"
http {
header = [{
name = "x-check"
exact = "exactbad"
ignore_case = true
}]
}
},
{
action = "deny"
http {
header = [{
name = "x-check"
prefix = "prebad-"
ignore_case = true
}]
}
},
{
action = "deny"
http {
header = [{
name = "x-check"
suffix = "-sufbad"
ignore_case = true
}]
}
},
// redundant with above case, but included for real-world example
// and to cover values containing ".".
{
action = "deny"
http {
header = [{
name = "Host"
suffix = "bad.com"
ignore_case = true
}]
}
}
]
}
'
register_services primary
gen_envoy_bootstrap s1 19000
gen_envoy_bootstrap s2 19001

@ -0,0 +1,6 @@
#!/bin/bash
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
# Allow for non-normalized path testing by using alternative container.
export SERVICE_CONTAINER="echo"

@ -0,0 +1,129 @@
#!/usr/bin/env bats
load helpers
@test "s1 proxy admin is up on :19000" {
retry_default curl -f -s localhost:19000/stats -o /dev/null
}
@test "s2 proxy admin is up on :19001" {
retry_default curl -f -s localhost:19001/stats -o /dev/null
}
@test "s1 proxy listener should be up and have right cert" {
assert_proxy_presents_cert_uri localhost:21000 s1
}
@test "s2 proxy listener should be up and have right cert" {
assert_proxy_presents_cert_uri localhost:21001 s2
}
@test "s2 proxies should be healthy" {
assert_service_has_healthy_instances s2 1
}
@test "s1 upstream should have healthy endpoints for s2" {
assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 1
}
@test "s2 should have http rbac rules loaded from xDS" {
retry_default assert_envoy_http_rbac_policy_count localhost:19001 1
}
# The following tests assert one of two things: that the request was
# rejected by L7 intentions as expected due to normalization, or that the
# request was allowed, and the request received by the upstream matched the
# expected normalized form.
@test "test allowed path" {
retry_default must_pass_http_request GET localhost:5000/foo
retry_default must_pass_http_request GET localhost:5000/value/foo
retry_default must_pass_http_request GET localhost:5000/foo/supersecret
}
@test "test disallowed path" {
retry_default must_fail_http_request 403 GET 'localhost:5000/value/supersecret'
retry_default must_fail_http_request 403 GET 'localhost:5000/value/supersecret#foo'
retry_default must_fail_http_request 403 GET 'localhost:5000/value/supersecret?'
}
@test "test disallowed path with repeat slashes" {
retry_default must_fail_http_request 403 GET 'localhost:5000/value//supersecret'
retry_default must_fail_http_request 403 GET 'localhost:5000/value///supersecret'
}
@test "test path with repeat slashes normalized" {
# After each request, verify that the request path observed by fortio matches the expected normalized path.
retry_default must_pass_http_request GET 'localhost:5000/value//foo'
get_echo_request_path | grep -Fx '/value/foo'
retry_default must_pass_http_request GET 'localhost:5000/value///foo'
get_echo_request_path | grep -Fx '/value/foo'
}
@test "test disallowed path with escaped characters" {
# escaped '/' (HTTP reserved)
retry_default must_fail_http_request 403 GET 'localhost:5000/value%2Fsupersecret'
# escaped 'v' (not HTTP reserved)
retry_default must_fail_http_request 403 GET 'localhost:5000/value/%73upersecret'
}
@test "test path with escaped characters normalized" {
# escaped '/' (HTTP reserved)
retry_default must_pass_http_request GET 'localhost:5000/value%2Ffoo'
get_echo_request_path | grep -Fx '/value/foo'
# escaped 'v' (not HTTP reserved)
retry_default must_pass_http_request GET 'localhost:5000/value/%66oo'
get_echo_request_path | grep -Fx '/value/foo'
}
@test "test disallowed path with backward slashes" {
# URLs must be quoted due to backslashes, otherwise shell erases them
retry_default must_fail_http_request 403 GET 'localhost:5000/value\supersecret'
retry_default must_fail_http_request 403 GET 'localhost:5000/value\\supersecret'
retry_default must_fail_http_request 403 GET 'localhost:5000/value\/supersecret'
retry_default must_fail_http_request 403 GET 'localhost:5000/value/\/supersecret'
}
@test "test path with backward slashes normalized" {
retry_default must_pass_http_request GET 'localhost:5000/value\foo'
get_echo_request_path | grep -Fx '/value/foo'
retry_default must_pass_http_request GET 'localhost:5000/value\\foo'
get_echo_request_path | grep -Fx '/value/foo'
retry_default must_pass_http_request GET 'localhost:5000/value\/foo'
get_echo_request_path | grep -Fx '/value/foo'
retry_default must_pass_http_request GET 'localhost:5000/value/\/foo'
get_echo_request_path | grep -Fx '/value/foo'
}
@test "test disallowed underscore in header key" {
# Envoy responds with 400 when configured to reject underscore headers.
retry_default must_fail_http_request 400 GET localhost:5000/foo x_poison:anything
retry_default must_fail_http_request 400 GET localhost:5000/foo x_check:bad
retry_default must_fail_http_request 400 GET localhost:5000/foo x_check:good-sufbad
retry_default must_fail_http_request 400 GET localhost:5000/foo x_check:prebad-good
}
@test "test disallowed contains header" {
retry_default must_fail_http_request 403 GET localhost:5000/foo x-check:thiscontainsbadinit
}
@test "test disallowed ignore case header" {
retry_default must_fail_http_request 403 GET localhost:5000/foo x-check:exactBaD
retry_default must_fail_http_request 403 GET localhost:5000/foo x-check:good-SuFBaD
retry_default must_fail_http_request 403 GET localhost:5000/foo x-check:PrEBaD-good
retry_default must_fail_http_request 403 GET localhost:5000/foo x-check:thiscontainsBaDinit
retry_default must_fail_http_request 403 GET localhost:5000/foo Host:foo.BaD.com
}
@test "test case-insensitive disallowed header" {
retry_default must_fail_http_request 403 GET localhost:5000/foo Host:foo.BAD.com
}
# @test "s1 upstream should NOT be able to connect to s2" {
# run retry_default must_fail_tcp_connection localhost:5000
# echo "OUTPUT $output"
# [ "$status" == "0" ]
# }

@ -51,6 +51,17 @@ sources {
}]
}
},
{
action = "allow"
http {
path_exact = "/hdr-exact-ignore-case"
header = [{
name = "x-test-debug"
exact = "foo.bar.com"
ignore_case = true
}]
}
},
{
action = "allow"
http {
@ -61,6 +72,17 @@ sources {
}]
}
},
{
action = "allow"
http {
path_exact = "/hdr-prefix-ignore-case"
header = [{
name = "x-test-debug"
prefix = "foo.bar"
ignore_case = true
}]
}
},
{
action = "allow"
http {
@ -71,6 +93,38 @@ sources {
}]
}
},
{
action = "allow"
http {
path_exact = "/hdr-suffix-ignore-case"
header = [{
name = "x-test-debug"
suffix = "bar.com"
ignore_case = true
}]
}
},
{
action = "allow"
http {
path_exact = "/hdr-contains"
header = [{
name = "x-test-debug"
contains = "contains"
}]
}
},
{
action = "allow"
http {
path_exact = "/hdr-contains-ignore-case"
header = [{
name = "x-test-debug"
contains = "contains"
ignore_case = true
}]
}
},
{
action = "allow"
http {

@ -34,49 +34,84 @@ load helpers
@test "test exact path" {
retry_default must_pass_http_request GET localhost:5000/exact
retry_default must_fail_http_request GET localhost:5000/exact-nope
retry_default must_fail_http_request 403 GET localhost:5000/exact-nope
}
@test "test prefix path" {
retry_default must_pass_http_request GET localhost:5000/prefix
retry_default must_fail_http_request GET localhost:5000/nope-prefix
retry_default must_fail_http_request 403 GET localhost:5000/nope-prefix
}
@test "test regex path" {
retry_default must_pass_http_request GET localhost:5000/regex
retry_default must_fail_http_request GET localhost:5000/reggex
retry_default must_fail_http_request 403 GET localhost:5000/reggex
}
@test "test present header" {
retry_default must_pass_http_request GET localhost:5000/hdr-present anything
retry_default must_fail_http_request GET localhost:5000/hdr-present ""
retry_default must_pass_http_request GET localhost:5000/hdr-present x-test-debug:anything
retry_default must_fail_http_request 403 GET localhost:5000/hdr-present x-test-debug:
}
@test "test exact header" {
retry_default must_pass_http_request GET localhost:5000/hdr-exact exact
retry_default must_fail_http_request GET localhost:5000/hdr-exact exact-nope
retry_default must_pass_http_request GET localhost:5000/hdr-exact x-test-debug:exact
retry_default must_fail_http_request 403 GET localhost:5000/hdr-exact x-test-debug:exact-nope
}
@test "test prefix header" {
retry_default must_pass_http_request GET localhost:5000/hdr-prefix prefix
retry_default must_fail_http_request GET localhost:5000/hdr-prefix nope-prefix
retry_default must_pass_http_request GET localhost:5000/hdr-prefix x-test-debug:prefix
retry_default must_fail_http_request 403 GET localhost:5000/hdr-prefix x-test-debug:nope-prefix
}
@test "test suffix header" {
retry_default must_pass_http_request GET localhost:5000/hdr-suffix suffix
retry_default must_fail_http_request GET localhost:5000/hdr-suffix suffix-nope
retry_default must_pass_http_request GET localhost:5000/hdr-suffix x-test-debug:suffix
retry_default must_fail_http_request 403 GET localhost:5000/hdr-suffix x-test-debug:suffix-nope
}
@test "test contains header" {
retry_default must_pass_http_request GET localhost:5000/hdr-contains x-test-debug:contains
retry_default must_pass_http_request GET localhost:5000/hdr-contains x-test-debug:ccontainss
retry_default must_pass_http_request GET localhost:5000/hdr-contains x-test-debug:still-contains-value
retry_default must_fail_http_request 403 GET localhost:5000/hdr-contains x-test-debug:conntains
}
@test "test regex header" {
retry_default must_pass_http_request GET localhost:5000/hdr-regex regex
retry_default must_fail_http_request GET localhost:5000/hdr-regex reggex
retry_default must_pass_http_request GET localhost:5000/hdr-regex x-test-debug:regex
retry_default must_fail_http_request 403 GET localhost:5000/hdr-regex x-test-debug:reggex
}
@test "test exact ignore case header" {
retry_default must_pass_http_request GET localhost:5000/hdr-exact-ignore-case x-test-debug:foo.bar.com
retry_default must_pass_http_request GET localhost:5000/hdr-exact-ignore-case x-test-debug:foo.BAR.com
retry_default must_pass_http_request GET localhost:5000/hdr-exact-ignore-case x-test-debug:fOo.bAr.coM
retry_default must_fail_http_request 403 GET localhost:5000/hdr-exact-ignore-case x-test-debug:fOo.bAr.coM.nope
}
@test "test prefix ignore case header" {
retry_default must_pass_http_request GET localhost:5000/hdr-prefix-ignore-case x-test-debug:foo.bar.com
retry_default must_pass_http_request GET localhost:5000/hdr-prefix-ignore-case x-test-debug:foo.BAR.com
retry_default must_pass_http_request GET localhost:5000/hdr-prefix-ignore-case x-test-debug:fOo.bAr.coM
retry_default must_fail_http_request 403 GET localhost:5000/hdr-prefix-ignore-case x-test-debug:nope.fOo.bAr.coM
}
@test "test suffix ignore case header" {
retry_default must_pass_http_request GET localhost:5000/hdr-suffix-ignore-case x-test-debug:foo.bar.com
retry_default must_pass_http_request GET localhost:5000/hdr-suffix-ignore-case x-test-debug:foo.BAR.com
retry_default must_pass_http_request GET localhost:5000/hdr-suffix-ignore-case x-test-debug:fOo.bAr.coM
retry_default must_fail_http_request 403 GET localhost:5000/hdr-suffix-ignore-case x-test-debug:fOo.bAr.coM.nope
}
@test "test contains ignore case header" {
retry_default must_pass_http_request GET localhost:5000/hdr-contains-ignore-case x-test-debug:cOntAins
retry_default must_pass_http_request GET localhost:5000/hdr-contains-ignore-case x-test-debug:CconTainsS
retry_default must_pass_http_request GET localhost:5000/hdr-contains-ignore-case x-test-debug:still-cOntAins-value
retry_default must_fail_http_request 403 GET localhost:5000/hdr-contains-ignore-case x-test-debug:cOnntAins
}
@test "test method match" {
retry_default must_pass_http_request GET localhost:5000/method-match
retry_default must_pass_http_request PUT localhost:5000/method-match
retry_default must_fail_http_request POST localhost:5000/method-match
retry_default must_fail_http_request HEAD localhost:5000/method-match
retry_default must_fail_http_request 403 POST localhost:5000/method-match
retry_default must_fail_http_request 403 HEAD localhost:5000/method-match
}
# @test "s1 upstream should NOT be able to connect to s2" {

@ -761,17 +761,13 @@ function must_fail_http_connection {
}
# must_pass_http_request allows you to craft a specific http request to assert
# that envoy will NOT reject the request. Primarily of use for testing L7
# intentions.
# that envoy will NOT reject the request.
function must_pass_http_request {
local METHOD=$1
local URL=$2
local DEBUG_HEADER_VALUE="${3:-""}"
shift 2
local extra_args
if [[ -n "${DEBUG_HEADER_VALUE}" ]]; then
extra_args="-H x-test-debug:${DEBUG_HEADER_VALUE}"
fi
case "$METHOD" in
GET) ;;
@ -786,22 +782,25 @@ function must_pass_http_request {
;;
esac
# Treat any remaining args as header KVs
for HEADER_ARG in "$@"; do
extra_args="$extra_args -H ${HEADER_ARG}"
done
run curl --no-keepalive -v -s -f $extra_args "$URL"
[ "$status" == 0 ]
}
# must_fail_http_request allows you to craft a specific http request to assert
# that envoy will reject the request. Primarily of use for testing L7
# intentions.
# that envoy will reject the request. Must supply the expected status code before
# method and URL.
function must_fail_http_request {
local METHOD=$1
local URL=$2
local DEBUG_HEADER_VALUE="${3:-""}"
local EXPECT_RESPONSE=$1
local METHOD=$2
local URL=$3
shift 2
local extra_args
if [[ -n "${DEBUG_HEADER_VALUE}" ]]; then
extra_args="-H x-test-debug:${DEBUG_HEADER_VALUE}"
fi
case "$METHOD" in
HEAD)
extra_args="$extra_args -I"
@ -819,12 +818,42 @@ function must_fail_http_request {
;;
esac
# Treat any remaining args as header KVs
for HEADER_ARG in "$@"; do
extra_args="$extra_args -H ${HEADER_ARG}"
done
# Attempt to curl through upstream
run curl --no-keepalive -s -i $extra_args "$URL"
echo "OUTPUT $output"
echo "$output" | grep "403 Forbidden"
# Output of curl should include status code immediately after 'HTTP/1.1'
echo "$output" | grep "HTTP/1.1 $EXPECT_RESPONSE"
}
# Gets the JSON response containing request parameters from the echo service response.
# See https://github.com/mendhak/docker-http-https-echo?tab=readme-ov-file#screenshots
# for example response body.
# Requires SERVICE_CONTAINER=echo.
function get_echo_output {
# Take the JSON response from $output, starting with first line containing only '{'
# and ending with the next line containing only '}'.
# The first sed converts a trailing '}* <some text...>' (curl -v output) to just '}'.
local json=$(echo "$output" | sed 's/}\*.*/}/' | sed -n -e '/^{$/,/^}$/{ p; }')
echo $json | jq -r '.' || echo "Output did not contain valid JSON: $output" >&3
}
# Gets the value of the raw request path from the echo service response.
# Requires SERVICE_CONTAINER=echo.
function get_echo_request_path {
get_echo_output | jq -r '.path'
}
# Gets the value of a given request header from the echo service response.
# Requires SERVICE_CONTAINER=echo.
function get_echo_request_header_value {
get_echo_output | jq -r ".headers.$1"
}
function gen_envoy_bootstrap {

@ -602,15 +602,43 @@ function run_container {
"run_container_$1"
}
# Run the common service container. By default, uses fortio/fortio.
#
# To use mendhak/http-https-echo, set SERVICE_CONTAINER=echo in vars.sh.
#
# To provide a custom docker run command for test containers, override
# common_run_container_service in vars.sh (which will be sourced prior to
# invocation). Use $(container_name_prev) in the custom function to get
# the correct effective container name. See common_run_container-fortio
# for the expected args list.
function common_run_container_service {
local service="$1"
local CLUSTER="$2"
local httpPort="$3"
local grpcPort="$4"
local serviceContainer=${SERVICE_CONTAINER:-fortio}
local containerName=$(container_name_prev)
docker run --sysctl net.ipv6.conf.all.disable_ipv6=1 -d --name $(container_name_prev) \
case "$serviceContainer" in
fortio)
common_run_container-fortio "$containerName" "$@"
;;
echo)
common_run_container-echo "$containerName" "$@"
;;
*)
echo "Unknown common run container: $runContainer"
return 1
;;
esac
}
function common_run_container-fortio {
local containerName="$1"
local service="$2"
local cluster="$3"
local httpPort="$4"
local grpcPort="$5"
docker run --sysctl net.ipv6.conf.all.disable_ipv6=1 -d --name $containerName \
-e "FORTIO_NAME=${service}" \
$(network_snippet $CLUSTER) \
$(network_snippet $cluster) \
"${HASHICORP_DOCKER_PROXY}/fortio/fortio" \
server \
-http-port ":$httpPort" \
@ -618,6 +646,25 @@ function common_run_container_service {
-redirect-port disabled >/dev/null
}
# Alternative to Fortio, which has limited ability to echo back arbitrary
# requests (only one pre-determined debug path), and uses Go's net/http, which
# force-normalizes paths. Useful for verifying HTTP request parameters sent by
# Envoy to the upstream.
function common_run_container-echo {
local containerName="$1"
local cluster="$3"
local httpPort="$4"
# HTTPS_PORT=0 will randomly assign a port number. It must be set, otherwise
# multiple containers on same network will fail due to using the same default port.
docker run --sysctl net.ipv6.conf.all.disable_ipv6=1 -d --name $containerName \
-e "HTTP_PORT=${httpPort}" \
-e "HTTPS_PORT=0" \
$(network_snippet $cluster) \
${HASHICORP_DOCKER_PROXY}/mendhak/http-https-echo:34 >/dev/null
}
function run_container_s1 {
common_run_container_service s1 primary 8080 8079
}

@ -69,7 +69,20 @@
</group.Element>
{{/if}}
{{#if shouldShowIgnoreCaseField}}
<group.Element
@name="IgnoreCase"
@error={{changeset-get changeset 'error.IgnoreCase'}}
as |el|>
<el.Label>Ignore Case</el.Label>
<el.Checkbox
checked={{if IgnoreCase 'checked'}}
onchange={{action 'change' 'IgnoreCase' changeset}}
/>
</group.Element>
{{/if}}
</div>
</fieldset>
</FormGroup>
</div>
</div>

@ -4,7 +4,7 @@
*/
import Component from '@ember/component';
import { get, set, computed } from '@ember/object';
import { set, computed } from '@ember/object';
import { alias, equal, not } from '@ember/object/computed';
import { inject as service } from '@ember/service';
@ -37,6 +37,7 @@ export default Component.extend({
Exact: 'Exactly Matching',
Prefix: 'Prefixed by',
Suffix: 'Suffixed by',
Contains: 'Containing',
Regex: 'Regular Expression',
Present: 'Is present',
};
@ -49,9 +50,14 @@ export default Component.extend({
headerTypeEqualsPresent: equal('headerType', 'Present'),
shouldShowValueField: not('headerTypeEqualsPresent'),
shouldShowIgnoreCaseField: computed('headerType', function () {
return this.headerType !== 'Present' && this.headerType !== 'Regex';
}),
actions: {
change: function (name, changeset, e) {
const value = typeof get(e, 'target.value') !== 'undefined' ? e.target.value : e;
const valueIndicator = e.target?.type === 'checkbox' ? e.target?.checked : e.target?.value;
const value = typeof valueIndicator !== 'undefined' ? valueIndicator : e;
switch (name) {
default:
changeset.set(name, value);
@ -65,6 +71,7 @@ export default Component.extend({
// Present is a boolean, whereas all other header types have a value
const value = changeset.HeaderType === 'Present' ? true : changeset.Value;
changeset.set(changeset.HeaderType, value);
changeset.set('IgnoreCase', changeset.IgnoreCase);
// this will prevent the changeset from overwriting the
// computed properties on the ED object

@ -11,7 +11,14 @@ export default (scope = '.consul-intention-permission-header-form') => {
scope: scope,
HeaderType: {
scope: '[data-property="headertype"]',
...powerSelect(['ExactlyMatching', 'PrefixedBy', 'SuffixedBy', 'RegEx', 'IsPresent']),
...powerSelect([
'ExactlyMatching',
'PrefixedBy',
'SuffixedBy',
'Containing',
'RegEx',
'IsPresent',
]),
},
Name: {
scope: '[data-property="name"] input',

@ -6,21 +6,26 @@
import { helper } from '@ember/component/helper';
export default helper(function routeMatch([item], hash) {
const prop = ['Present', 'Exact', 'Prefix', 'Suffix', 'Regex'].find(
const prop = ['Present', 'Exact', 'Prefix', 'Suffix', 'Contains', 'Regex'].find(
(prop) => typeof item[prop] !== 'undefined'
);
let invertPrefix = item.Invert ? 'NOT ' : '';
let ignoreCaseSuffix = item.IgnoreCase ? ' (case-insensitive)' : '';
switch (prop) {
case 'Present':
return `${item.Invert ? `NOT ` : ``}present`;
return `${invertPrefix}present`;
case 'Exact':
return `${item.Invert ? `NOT ` : ``}exactly matching "${item.Exact}"`;
return `${invertPrefix}exactly matching "${item.Exact}"${ignoreCaseSuffix}`;
case 'Prefix':
return `${item.Invert ? `NOT ` : ``}prefixed by "${item.Prefix}"`;
return `${invertPrefix}prefixed by "${item.Prefix}"${ignoreCaseSuffix}`;
case 'Suffix':
return `${item.Invert ? `NOT ` : ``}suffixed by "${item.Suffix}"`;
return `${invertPrefix}suffixed by "${item.Suffix}"${ignoreCaseSuffix}`;
case 'Contains':
return `${invertPrefix}containing "${item.Contains}"${ignoreCaseSuffix}`;
case 'Regex':
return `${item.Invert ? `NOT ` : ``}matching the regex "${item.Regex}"`;
return `${invertPrefix}matching the regex "${item.Regex}"`;
}
return '';
});

@ -13,7 +13,7 @@ export const schema = {
required: true,
},
HeaderType: {
allowedValues: ['Exact', 'Prefix', 'Suffix', 'Regex', 'Present'],
allowedValues: ['Exact', 'Prefix', 'Suffix', 'Contains', 'Regex', 'Present'],
},
};
@ -23,11 +23,13 @@ export default class IntentionPermission extends Fragment {
@attr('string') Exact;
@attr('string') Prefix;
@attr('string') Suffix;
@attr('string') Contains;
@attr('string') Regex;
// this is a boolean but we don't want it to automatically be set to false
@attr() Present;
@or(...schema.HeaderType.allowedValues) Value;
@attr('boolean') IgnoreCase;
@computed(...schema.HeaderType.allowedValues)
get HeaderType() {

@ -78,11 +78,15 @@ ${range(headerCount).map(item => `
${fake.random.boolean() ? `
"Invert": true,
` : ``}
${fake.random.boolean() ? `
"IgnoreCase": true,
` : ``}
${fake.helpers.randomize([
'"Present": true',
'"Exact": "abc"',
'"Prefix": "abc"',
'"Suffix": "xyz"',
'"Contains": "abc"',
'"Regex": "[abc]"'
])}
}

@ -63,11 +63,15 @@ ${range(headerCount).map(item => `
${fake.random.boolean() ? `
"Invert": true,
` : ``}
${fake.random.boolean() ? `
"IgnoreCase": true,
` : ``}
${fake.helpers.randomize([
'"Present": true',
'"Exact": "abc"',
'"Prefix": "abc"',
'"Suffix": "xyz"',
'"Contains": "abc"',
'"Regex": "[abc]"'
])}
}

@ -69,11 +69,15 @@ ${range(headerCount).map(item => `
${fake.random.boolean() ? `
"Invert": true,
` : ``}
${fake.random.boolean() ? `
"IgnoreCase": true,
` : ``}
${fake.helpers.randomize([
'"Present": true',
'"Exact": "abc"',
'"Prefix": "abc"',
'"Suffix": "xyz"',
'"Contains": "abc"',
'"Regex": "[abc]"'
])}
}

@ -264,6 +264,58 @@ spec:
Note that the Kubernetes example does not include a `partition` field. Configuration entries are applied on Kubernetes using [custom resource definitions (CRD)](/consul/docs/k8s/crds), which can only be scoped to their own partition.
### Request Normalization
Enable options under `HTTP.Incoming.RequestNormalization` to apply normalization to all inbound traffic to mesh proxies.
<CodeTabs tabs={[ "HCL", "Kubernetes YAML", "JSON" ]}>
```hcl
Kind = "mesh"
HTTP {
Incoming {
RequestNormalization {
InsecureDisablePathNormalization = false // default false, shown for completeness
MergeSlashes = true
PathWithEscapedSlashesAction = "UNESCAPE_AND_FORWARD"
HeadersWithUnderscoresAction = "REJECT_REQUEST"
}
}
}
```
```yaml
apiVersion: consul.hashicorp.com/v1alpha1
kind: Mesh
metadata:
name: mesh
spec:
http:
incoming:
requestNormalization:
insecureDisablePathNormalization: false # default false, shown for completeness
mergeSlashes: true
pathWithEscapedSlashesAction: UNESCAPE_AND_FORWARD
headersWithUnderscoresAction: REJECT_REQUEST
```
```json
{
"Kind": "mesh",
"HTTP": {
"Incoming": {
"RequestNormalization": {
"InsecureDisablePathNormalization": false,
"MergeSlashes": true,
"PathWithEscapedSlashesAction": "UNESCAPE_AND_FORWARD",
"HeadersWithUnderscoresAction": "REJECT_REQUEST"
}
}
}
}
```
</CodeTabs>
## Available Fields
@ -452,6 +504,57 @@ Note that the Kubernetes example does not include a `partition` field. Configura
for all Envoy proxies. As a result, Consul will not include the \`x-forwarded-client-cert\` header in the next hop.
If set to \`false\` (default), the XFCC header is propagated to upstream applications.`,
},
{
name: 'Incoming',
type: 'DirectionalHTTPConfig: <optional>',
description: `HTTP configuration for inbound traffic to mesh proxies.`,
children: [
{
name: 'RequestNormalization',
type: 'RequestNormalizationConfig: <optional>',
description: `Request normalization configuration for inbound traffic to mesh proxies.`,
children: [
{
name: 'InsecureDisablePathNormalization',
type: 'bool: false',
description: `Sets the value of the \`normalize_path\` option in the Envoy listener's \`HttpConnectionManager\`. The default value is \`false\`.
When set to \`true\` in Consul, \`normalize_path\` is set to \`false\` for the Envoy proxy.
This parameter disables the normalization of request URL paths according to RFC 3986,
conversion of \`\\\` to \`/\`, and decoding non-reserved %-encoded characters. When using L7
intentions with path match rules, we recommend enabling path normalization in order
to avoid match rule circumvention with non-normalized path values.`,
},
{
name: 'MergeSlashes',
type: 'bool: false',
description: `Sets the value of the \`merge_slashes\` option in the Envoy listener's \`HttpConnectionManager\`. The default value is \`false\`.
This option controls the normalization of request URL paths by merging consecutive \`/\` characters. This normalization is not part
of RFC 3986. When using L7 intentions with path match rules, we recommend enabling this setting to avoid match rule circumvention through 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.`,
},
{
name: 'PathWithEscapedSlashesAction',
type: 'string: ""',
description: `Sets the value of the \`path_with_escaped_slashes_action\` option in the Envoy listener's
\`HttpConnectionManager\`. The default value of this option is empty, which is
equivalent to \`IMPLEMENTATION_SPECIFIC_DEFAULT\`. This parameter controls the action taken in response to request URL paths with escaped
slashes in the path. When using L7 intentions with path match rules, we recommend enabling this setting to avoid match rule circumvention through 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. Refer to the Envoy documentation for more information on available
options.`,
},
{
name: 'HeadersWithUnderscoresAction',
type: 'string: ""',
description: `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\`. Refer to the Envoy documentation for more information on available options.`,
},
],
},
],
}
],
},
{

@ -96,7 +96,9 @@ The following outline shows how to format the service intentions configuration e
- [`exact`](#spec-sources-permissions-http-header): string | no default
- [`prefix`](#spec-sources-permissions-http-header): string | no default
- [`suffix`](#spec-sources-permissions-http-header): string | no default
- [`contains`](#spec-sources-permissions-http-header): string | no default
- [`regex`](#spec-sources-permissions-http-header): string | no default
- [`ignoreCase`](#spec-sources-permissions-http-header): boolean | `false`
- [`invert`](#spec-sources-permissions-http-header): boolean | `false`
- [`description`](#spec-sources-description): string
@ -156,18 +158,31 @@ Sources = [
{
Name = "<http header name>" # string
Present = <true or false> # boolean
Invert = <true or false> # boolean
},
{
Name = "<http header name>" # string
Exact = "<header-value>" # boolean
IgnoreCase = <true or false> # boolean
Invert = <true or false> # boolean
},
{
Name = "<http header name>" # string
Prefix = "<source header value prefix>" # string
IgnoreCase = <true or false> # boolean
Invert = <true or false> # boolean
},
{
Name = "<http header name>" # string
Suffix = "<source header value suffix>" # string
IgnoreCase = <true or false> # boolean
Invert = <true or false> # boolean
},
{
Name = "<http header name>" # string
Contains = "<value to search for>" # string
IgnoreCase = <true or false> # boolean
Invert = <true or false> # boolean
},
{
Name = "<http header name>" # string
@ -227,12 +242,23 @@ spec:
header:
- name: <http header name>
present: true
invert: false
- name: <http header name>
exact: false
exact: <header-value>
ignoreCase: false
invert: false
- name: <http header name>
prefix: <source header value prefix>
ignoreCase: false
invert: false
- name: <http header name>
suffix: <source header value suffix>
ignoreCase: false
invert: false
- name: <http header name>
contains: <value to search for>
ignoreCase: false
invert: false
- name: <http header name>
regex: <regex pattern to match>
invert: false
@ -287,19 +313,32 @@ spec:
"Header":[
{
"Name":"<http header name>",
"Present":true
"Present":true,
"Invert":false
},
{
"Name":"<http header name>",
"Exact":"<header-value>",
"IgnoreCase":false,,
"Invert":false
},
{
"Name":"<http header name>",
"Exact":false
"Prefix":"<source header value prefix>",
"IgnoreCase":false,
"Invert":false
},
{
"Name":"<http header name>",
"Prefix":"<source header value prefix>"
"Suffix":"<source header value suffix>",
"IgnoreCase":false,
"Invert":false
},
{
"Name":"<http header name>",
"Suffix":"<source header value suffix>"
"Contains":"<value to search for>",
"IgnoreCase":false,
"Invert":false
},
{
"Name":"<http header name>",
@ -923,16 +962,22 @@ Specifies a set of criteria for matching HTTP request headers. The request heade
- Default: None
- Data type: List of maps
Each member of the `header` list is a map that contains a `name` field and at least one match criterion. The following table describes the parameters that each member of the `header` list may contain:
Each member of the `header` list is a map that contains a `name` field and at least one match criterion.
~> **Warning**: If it is possible for a header to contain multiple values, we recommend using `contains` or `regex` rather than `exact`, `prefix`, or `suffix`. Envoy internally concatenates multiple header values into a single CSV value prior to applying match rules, which may result in match rules that depend on the beginning or end of a string vulnerable to circumvention. A more robust alternative is using `contains` or, if a stricter value match is required, configuring a regex pattern that is tolerant of comma-separated values.
The following table describes the parameters that each member of the `header` list may contain:
| Parameter | Description | Data type | Required |
| --- | --- | --- | --- |
| `name` | Specifies the name of the header to match. | string | required |
| `present` | Enables a match if the header configured in the `name` field appears in the request. Consul matches on any value as long as the header key appears in the request. Do not specify `present` if `exact`, `prefix`, `suffix`, or `regex` are configured in the same `header` configuration. | boolean | optional |
| `Exact` | Specifies a value for the header key set in the `Name` field. If the request header value matches the `exact` value, Consul applies the permission. Do not specify `exact` if `present`, `prefix`, `suffix`, or `regex` are configured in the same `header` configuration. | string | optional |
| `prefix` | Specifies a prefix value for the header key set in the `name` field. If the request header value starts with the `prefix` value, Consul applies the permission. Do not specify `prefix` if `present`, `exact`, `suffix`, or `regex` are configured in the same `header` configuration. | string | optional |
| `suffix` | Specifies a suffix value for the header key set in the `name` field. If the request header value ends with the `suffix` value, Consul applies the permission. Do not specify `suffix` if `present`, `exact`, `prefix`, or `regex` are configured in the same `header` configuration. | string | optional |
| `regex` | Specifies a regular expression pattern as the value for the header key set in the `name` field. If the request header value matches the regex, Consul applies the permission. Do not specify `regex` if `present`, `exact`, `prefix`, or `suffix` are configured in the same `header` configuration. The regex syntax is proxy-specific. If using Envoy, refer to the [re2 documentation](https://github.com/google/re2/wiki/Syntax) for details. | string | optional |
| `present` | Enables a match if the header configured in the `name` field appears in the request. Consul matches on any value as long as the header key appears in the request. Do not specify `present` if `exact`, `prefix`, `suffix`, `contains`, or `regex` are configured in the same `header` configuration. | boolean | optional |
| `Exact` | Specifies a value for the header key set in the `Name` field. If the request header value matches the `exact` value, Consul applies the permission. Do not specify `exact` if `present`, `prefix`, `suffix`, `contains`, or `regex` are configured in the same `header` configuration. | string | optional |
| `prefix` | Specifies a prefix value for the header key set in the `name` field. If the request header value starts with the `prefix` value, Consul applies the permission. Do not specify `prefix` if `present`, `exact`, `suffix`, `contains`, or `regex` are configured in the same `header` configuration. | string | optional |
| `suffix` | Specifies a suffix value for the header key set in the `name` field. If the request header value ends with the `suffix` value, Consul applies the permission. Do not specify `suffix` if `present`, `exact`, `prefix`, `contains`, or `regex` are configured in the same `header` configuration. | string | optional |
| `contains` | Specifies a contains value for the header key set in the `name` field. If the request header value includes the `contains` value, Consul applies the permission. Do not specify `contains` if `present`, `exact`, `prefix`, `suffix`, or `regex` are configured in the same `header` configuration. | string | optional |
| `regex` | Specifies a regular expression pattern as the value for the header key set in the `name` field. If the request header value matches the regex, Consul applies the permission. Do not specify `regex` if `present`, `exact`, `prefix`, `suffix`, or `contains` are configured in the same `header` configuration. The regex syntax is proxy-specific. If using Envoy, refer to the [re2 documentation](https://github.com/google/re2/wiki/Syntax) for details. | string | optional |
| `ignoreCase` | Ignores the case of the provided header value when matching with exact, prefix, suffix, or contains. Default is `false`. | boolean | optional |
| `invert` | Inverts the matching logic configured in the `header`. Default is `false`. | boolean | optional |
### `spec.sources[].type`

@ -36,7 +36,11 @@ application](/consul/docs/connect/native) enforces intentions on inbound connect
L4 intentions mediate the ability to establish new connections. Modifying an intention does not have an effect on existing connections. As a result, changing a connection from `allow` to `deny` does not sever the connection.
L7 intentions mediate the ability to issue new requests. When an intention is modified, requests received after the modification use the latest intention rules to enforce access. Changing a connection from `allow` to `deny` does not sever the connection, but doing so blocks new requests from being processed.
L7 intentions mediate the ability to issue new requests. When an intention is modified, requests received after the modification use the latest intention rules to enforce access. Changing a connection from `allow` to `deny` does not sever the connection, but doing so blocks new requests from being processed.
When using L7 intentions, we recommend that you review and update the [Mesh request normalization configuration](/consul/docs/connect/security#request-normalization-and-configured) to avoid unintended match rule circumvention. More details are available in the [Mesh configuration entry reference](/consul/docs/connect/config-entries/mesh#request-normalization).
When you use L7 intentions with header matching and it is possible for a header to contain multiple values, we recommend using `contains` or `regex` instead of `exact`, `prefix`, or `suffix`. For more information, refer to the [service intentions configuration entry reference](/consul/docs/connect/config-entries/service-intentions#spec-sources-permissions-http-header).
### Caching

@ -32,10 +32,38 @@ Consul should be configured with a default deny intention policy. This forces
all service-to-service communication to be explicitly
allowed via an allow [intention](/consul/docs/connect/intentions).
One advantage of using a default deny policy in combination with specific "allow" rules
is that a failure of intentions due to misconfiguration always results in
_denied_ traffic, rather than unwanted _allowed_ traffic.
In the absence of `default_intention_policy` Consul will fall back to the ACL
default policy when determining whether to allow or deny communications without
an explicit intention.
### Request Normalization Configured for L7 Intentions
Atypical traffic patterns may interfere with the enforcement of L7 intentions. For
example, if a service makes request to a non-normalized URI path and Consul is not
configured to force path normalization, it becomes possible to circumvent path match rules. While a
default deny policy can limit the impact of this issue, we still recommend
that you review your current request normalization configuration. Normalization is critical to avoid unwanted
traffic, especially when using unrecommended security options such as a default allow intentions policy.
Consul adopts a default normalization mode that adheres to [RFC 3986](
https://tools.ietf.org/html/rfc3986#section-6), but additional options to enable stricter
normalization are available in the cluster-wide [Mesh configuration entry](
/consul/docs/connect/config-entries/mesh). We recommend reviewing these options and
enabling the strictest set that does not interfere with application traffic.
We also recommend that you review L7 intention header match rules for potential
issues with multiple header values. Refer to the [service intentions
configuration entry reference](/consul/docs/connect/config-entries/service-intentions#spec-sources-permissions-http-header)
for more information.
You do not need to enable request normalization if you are not using L7 intentions.
However, normalization may also benefit the use of other service mesh features that
rely on L7 attribute matching, such as [service routers](/consul/docs/connect/manage-traffic#routing).
### ACLs Enabled with Default Deny
Consul must be configured to use ACLs with a default deny policy. This forces
@ -51,6 +79,10 @@ this. **If ACLs are not enabled**, deny intentions will still be enforced, but a
may edit intentions. This renders the security of the created intentions
effectively useless.
The advantage of a default deny policy in combination with specific "allow" rules
is that at worst, a failure of intentions due to misconfiguration will result in
_denied_ traffic, rather than unwanted _allowed_ traffic.
### TCP and UDP Encryption Enabled
TCP and UDP encryption must be enabled to prevent plaintext communication

@ -26,6 +26,10 @@ environment, but the general mechanisms for a secure Consul deployment revolve a
[authentication methods](/consul/docs/security/acl/auth-methods) can be used to enable trusted external parties to authorize
ACL token creation.
- **Intentions** - If in use, configure service intentions to use a default-deny policy. If L7 intentions are
in use, enable [Mesh request normalization](/consul/docs/connect/config-entries/mesh#request-normalization)
and review your [header match rules](/consul/docs/connect/config-entries/service-intentions#spec-sources-permissions-http-header) to prevent malformed requests from bypassing intentions.
- **Namespaces** <EnterpriseAlert inline /> - Read and write operations can be scoped to a logical namespace to restrict
access to Consul components within a multi-tenant environment.
@ -178,6 +182,13 @@ environment and adapt these configurations accordingly.
- **🏷 Namespace** <EnterpriseAlert inline /> - a named, logical scoping of Consul Enterprise resources, typically to
enable multi-tenant environments. Consul CE clusters always operate within the "default" namespace.
- **Intentions** - Service intentions control traffic communication between services at the network layer (L4) and
application layer (L7). If in use, we strongly recommend configuring intentions to use a default-deny policy.
When L7 intentions are in use, review your configuration for [Mesh request normalization](/consul/docs/connect/config-entries/mesh#request-normalization)
and use the strictest set of options suitable to your environment. At minimum, we
recommend keeping path normalization enabled, because this default setting prevents requests that do not conform to [RFC 3986](
https://tools.ietf.org/html/rfc3986#section-6) from bypassing path match rules.
- **Gossip Encryption** - A shared, base64-encoded 32-byte symmetric key is required to [encrypt Serf gossip
communication](/consul/tutorials/security/gossip-encryption-secure?utm_source=consul.io&utm_medium=docs) within a cluster using
AES GCM. The key size determines which AES encryption types to use; 16, 24, or 32 bytes to select AES-128, AES-192,
@ -252,6 +263,10 @@ environment and adapt these configurations accordingly.
}
```
- **Customize Mesh HTTP Request Normalization** - If L7 intentions are in use, we recommend configuring request normalization to
avoid match rule circumvention. Other normalization options, such as dropping or rejecting headers with underscores,
may also be appropriate depending on your requirements. Review the options in the [Mesh configuration entry](/consul/docs/connect/config-entries/mesh#request-normalization) to determine the appropriate settings for your use case.
- **Customize Default Limits** - Consul has a number of builtin features with default connection limits that should be
tuned to fit your environment.

@ -43,6 +43,15 @@ The Kubernetes-only legacy API gateway is superseded by the modern, multi-runtim
[API gateway](/consul/docs/connect/config-entries/api-gateway).
On Kubernetes, the modern API gateway is associated with the `connectInject.apiGateway` stanza.
### Mesh traffic request path normalization enabled by default
As of Consul v1.19.3, inbound traffic to mesh proxies will have Envoy request [path normalization](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-normalize-path) applied by default. This should not interfere with the majority of service traffic, but can be disabled if needed by setting `http.incoming.request_normalization.insecure_disable_path_normalization` to `true` in the [global `mesh` configuration entry](/consul/docs/connect/config-entries/mesh#request-normalization). This setting is generally safe to change if not using L7 intentions with path matching.
## Consul 1.18.x
### Mesh traffic request path normalization enabled by default
As of Consul v1.18.5, inbound traffic to mesh proxies will have Envoy request [path normalization](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-normalize-path) applied by default. This should not interfere with the majority of service traffic, but can be disabled if needed by setting `http.incoming.request_normalization.insecure_disable_path_normalization` to `true` in the [global `mesh` configuration entry](/consul/docs/connect/config-entries/mesh#request-normalization). This setting is generally safe to change if not using L7 intentions with path matching.
## Consul 1.17.x
@ -65,6 +74,10 @@ service-defaults are configured in each partition and namespace before upgrading
#### ACL tokens with templated policies
[ACL templated policies](/consul/docs/security/acl#templated-policies) were added to 1.17.0 to simplify obtaining the right permissions for ACL tokens. When performing a [rolling upgrade](/consul/tutorials/datacenter-operations/upgrade-federated-environment#server-rolling-upgrade) and a version of Consul prior to 1.17.x is presented with a token created Consul v1.17.x or newer that contains templated policies, the templated policies field is not recognized. As a result, the token might not have the expected permissions on the older version of Consul.
### Mesh traffic request path normalization enabled by default
As of Consul v1.17.8, inbound traffic to mesh proxies will have Envoy request [path normalization](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-normalize-path) applied by default. This should not interfere with the majority of service traffic, but can be disabled if needed by setting `http.incoming.request_normalization.insecure_disable_path_normalization` to `true` in the [global `mesh` configuration entry](/consul/docs/connect/config-entries/mesh#request-normalization). This setting is generally safe to change if not using L7 intentions with path matching.
## Consul 1.16.x
### Known issues
@ -241,6 +254,10 @@ In Consul v1.15 and higher:
</CodeBlockConfig>
### Mesh traffic request path normalization enabled by default
As of Consul v1.15.15, inbound traffic to mesh proxies will have Envoy request [path normalization](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-normalize-path) applied by default. This should not interfere with the majority of service traffic, but can be disabled if needed by setting `http.incoming.request_normalization.insecure_disable_path_normalization` to `true` in the [global `mesh` configuration entry](/consul/docs/connect/config-entries/mesh#request-normalization). This setting is generally safe to change if not using L7 intentions with path matching.
## Consul 1.14.x
### Service Mesh Compatibility

Loading…
Cancel
Save