Merge pull request #12878 from hashicorp/ma/x-forwarded-client-cert

Support x-forwarded-client-cert
This commit is contained in:
Mark Anderson 2022-05-04 11:05:44 -07:00 committed by GitHub
commit 1497421b65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 333 additions and 12 deletions

3
.changelog/12878.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
xds: Envoy now inserts x-forwarded-client-cert for incoming proxy connections
```

View File

@ -15,6 +15,8 @@ type MeshConfigEntry struct {
TLS *MeshTLSConfig `json:",omitempty"`
HTTP *MeshHTTPConfig `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex
@ -42,6 +44,10 @@ type MeshDirectionalTLSConfig struct {
CipherSuites []types.TLSCipherSuite `json:",omitempty" alias:"cipher_suites"`
}
type MeshHTTPConfig struct {
SanitizeXForwardedClientCert bool `alias:"sanitize_x_forwarded_client_cert"`
}
func (e *MeshConfigEntry) GetKind() string {
return MeshConfig
}

View File

@ -1694,6 +1694,9 @@ func TestDecodeConfigEntry(t *testing.T) {
]
}
}
http {
sanitize_x_forwarded_client_cert = true
}
`,
camel: `
Kind = "mesh"
@ -1722,6 +1725,9 @@ func TestDecodeConfigEntry(t *testing.T) {
]
}
}
HTTP {
SanitizeXForwardedClientCert = true
}
`,
expect: &MeshConfigEntry{
Meta: map[string]string{
@ -1749,6 +1755,9 @@ func TestDecodeConfigEntry(t *testing.T) {
},
},
},
HTTP: &MeshHTTPConfig{
SanitizeXForwardedClientCert: true,
},
},
},
{

View File

@ -859,6 +859,10 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
if err != nil {
return nil, err
}
if meshConfig := cfgSnap.MeshConfig(); meshConfig == nil || meshConfig.HTTP == nil || !meshConfig.HTTP.SanitizeXForwardedClientCert {
filterOpts.forwardClientDetails = true
filterOpts.forwardClientPolicy = envoy_http_v3.HttpConnectionManager_APPEND_FORWARD
}
}
filter, err := makeListenerFilter(filterOpts)
if err != nil {
@ -1146,6 +1150,12 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(
opts.cluster = ""
opts.useRDS = true
if meshConfig := cfgSnap.MeshConfig(); meshConfig == nil || meshConfig.HTTP == nil || !meshConfig.HTTP.SanitizeXForwardedClientCert {
opts.forwardClientDetails = true
// This assumes that we have a client cert (mTLS) (implied by the context of this function)
opts.forwardClientPolicy = envoy_http_v3.HttpConnectionManager_APPEND_FORWARD
}
}
filter, err := makeListenerFilter(opts)
@ -1376,6 +1386,8 @@ type listenerFilterOpts struct {
requestTimeoutMs *int
ingressGateway bool
httpAuthzFilter *envoy_http_v3.HttpFilter
forwardClientDetails bool
forwardClientPolicy envoy_http_v3.HttpConnectionManager_ForwardClientCertDetails
}
func makeListenerFilter(opts listenerFilterOpts) (*envoy_listener_v3.Filter, error) {
@ -1513,6 +1525,18 @@ func makeHTTPFilter(opts listenerFilterOpts) (*envoy_listener_v3.Filter, error)
cfg.Http2ProtocolOptions = &envoy_core_v3.Http2ProtocolOptions{}
}
// Note the default leads to setting HttpConnectionManager_SANITIZE
if opts.forwardClientDetails {
cfg.ForwardClientCertDetails = opts.forwardClientPolicy
cfg.SetCurrentClientCertDetails = &envoy_http_v3.HttpConnectionManager_SetCurrentClientCertDetails{
Subject: &wrappers.BoolValue{Value: true},
Cert: true,
Chain: true,
Dns: true,
Uri: true,
}
}
// Like injectConnectFilters for L4, here we ensure that the first filter
// (other than the "envoy.grpc_http1_bridge" filter) in the http filter
// chain of a public listener is the authz filter to prevent unauthorized

View File

@ -166,6 +166,27 @@ func TestListenersFromSnapshot(t *testing.T) {
}, nil)
},
},
{
name: "http-public-listener-no-xfcc",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshot(t,
func(ns *structs.NodeService) {
ns.Proxy.Config["protocol"] = "http"
},
[]cache.UpdateEvent{
{
CorrelationID: "mesh",
Result: &structs.ConfigEntryResponse{
Entry: &structs.MeshConfigEntry{
HTTP: &structs.MeshHTTPConfig{
SanitizeXForwardedClientCert: true,
},
},
},
},
})
},
},
{
name: "http-listener-with-timeouts",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {

View File

@ -67,6 +67,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "public_listener",
"routeConfig": {
"name": "public_listener",

View File

@ -0,0 +1,151 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "db:127.0.0.1:9191",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 9191
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "upstream.db.default.default.dc1",
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "prepared_query:geo-cache:127.10.10.10:8181",
"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",
"statPrefix": "upstream.prepared_query_geo-cache",
"cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "public_listener:0.0.0.0:9999",
"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",
"statPrefix": "public_listener",
"routeConfig": {
"name": "public_listener",
"virtualHosts": [
{
"name": "public_listener",
"domains": [
"*"
],
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "local_app"
}
}
]
}
]
},
"httpFilters": [
{
"name": "envoy.filters.http.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
}
}
},
{
"name": "envoy.filters.http.router",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
}
}
],
"tracing": {
"randomSampling": {
}
}
}
}
],
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
"commonTlsContext": {
"tlsParams": {
},
"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"
}
}
],
"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
}
}
}
],
"trafficDirection": "INBOUND"
}
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"nonce": "00000001"
}

View File

@ -67,6 +67,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "public_listener",
"routeConfig": {
"name": "public_listener",

View File

@ -184,6 +184,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "upstream.web.default.default.dc1",
"rds": {
"configSource": {
@ -258,6 +266,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "upstream.web.default.default.dc1",
"rds": {
"configSource": {
@ -332,6 +348,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "upstream.web.default.default.dc1",
"rds": {
"configSource": {

View File

@ -130,6 +130,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "upstream.web.default.default.dc1",
"rds": {
"configSource": {
@ -212,6 +220,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "upstream.web.default.default.dc1",
"rds": {
"configSource": {
@ -348,6 +364,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "upstream.web.default.default.dc1",
"rds": {
"configSource": {

View File

@ -184,6 +184,14 @@
"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
"forwardClientCertDetails": "APPEND_FORWARD",
"setCurrentClientCertDetails": {
"cert": true,
"chain": true,
"dns": true,
"subject": true,
"uri": true
},
"statPrefix": "upstream.web.default.default.dc1",
"rds": {
"configSource": {

View File

@ -1,6 +1,8 @@
package api
import "encoding/json"
import (
"encoding/json"
)
// MeshConfigEntry manages the global configuration for all service mesh
// proxies.
@ -19,6 +21,8 @@ type MeshConfigEntry struct {
TLS *MeshTLSConfig `json:",omitempty"`
HTTP *MeshHTTPConfig `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
// CreateIndex is the Raft index this entry was created at. This is a
@ -46,6 +50,10 @@ type MeshDirectionalTLSConfig struct {
CipherSuites []string `json:",omitempty" alias:"cipher_suites"`
}
type MeshHTTPConfig struct {
SanitizeXForwardedClientCert bool `alias:"sanitize_x_forwarded_client_cert"`
}
func (e *MeshConfigEntry) GetKind() string { return MeshConfig }
func (e *MeshConfigEntry) GetName() string { return MeshConfigMesh }
func (e *MeshConfigEntry) GetPartition() string { return e.Partition }

View File

@ -1278,6 +1278,9 @@ func TestDecodeConfigEntry(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
]
}
},
"HTTP": {
"SanitizeXForwardedClientCert": true
}
}
`,
@ -1307,6 +1310,9 @@ func TestDecodeConfigEntry(t *testing.T) {
},
},
},
HTTP: &MeshHTTPConfig{
SanitizeXForwardedClientCert: true,
},
},
},
} {

View File

@ -126,6 +126,9 @@ meta {
transparent_proxy {
mesh_destinations_only = true
}
http {
sanitize_x_forwarded_client_cert = true
}
`)
ui := cli.NewMockUi()
@ -143,6 +146,9 @@ transparent_proxy {
proxy, ok := entry.(*api.MeshConfigEntry)
require.True(t, ok)
require.Equal(t, map[string]string{"foo": "bar", "gir": "zim"}, proxy.Meta)
require.True(t, proxy.TransparentProxy.MeshDestinationsOnly)
require.True(t, proxy.HTTP.SanitizeXForwardedClientCert)
})
}

View File

@ -359,6 +359,21 @@ Note that the Kubernetes example does not include a `partition` field. Configura
},
],
},
{
name: 'HTTP',
type: 'HTTPConfig: <optional>',
description: 'HTTP configuration for the service mesh.',
children: [
{
name: 'SanitizeXForwardedClientCert',
yaml: false,
type: 'bool: <optional>',
description: `Set the envoy \`forward_client_cert_details\` option to \`SANITIZE\` for all proxies. This
configures Envoy to not send the \`x-forwarded-client-cert\` header to the next hop. If
unspecified or \`false\`, the XFCC header is propagated to upstream applications.`,
},
],
},
]}
/>