Permissive mTLS (#17035)

This implements permissive mTLS , which allows toggling services into "permissive" mTLS mode.
Permissive mTLS mode allows incoming "non Consul-mTLS" traffic to be forward unmodified to the application.

* Update service-defaults and proxy-defaults config entries with a MutualTLSMode field
* Update the mesh config entry with an AllowEnablingPermissiveMutualTLS field and implement the necessary validation. AllowEnablingPermissiveMutualTLS must be true to allow changing to MutualTLSMode=permissive, but this does not require that all proxy-defaults and service-defaults are currently in strict mode.
* Update xDS listener config to add a "permissive filter chain" when MutualTLSMode=permissive for a particular service. The permissive filter chain matches incoming traffic by the destination port. If the destination port matches the service port from the catalog, then no mTLS is required and the traffic sent is forwarded unmodified to the application.
This commit is contained in:
Paul Glass 2023-04-19 14:45:00 -05:00 committed by GitHub
parent 5e019393d3
commit d8d89d4b59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2101 additions and 1321 deletions

3
.changelog/17035.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
mesh: Add new permissive mTLS mode that allows sidecar proxies to forward incoming traffic unmodified to the application. This adds `AllowEnablingPermissiveMutualTLS` setting to the mesh config entry and the `MutualTLSMode` setting to proxy-defaults and service-defaults.
```

View File

@ -154,6 +154,10 @@ func MergeServiceConfig(defaults *structs.ServiceConfigResponse, service *struct
ns.Proxy.TransparentProxy.DialedDirectly = defaults.TransparentProxy.DialedDirectly
}
if ns.Proxy.MutualTLSMode == structs.MutualTLSModeDefault {
ns.Proxy.MutualTLSMode = defaults.MutualTLSMode
}
// remoteUpstreams contains synthetic Upstreams generated from central config (service-defaults.UpstreamConfigs).
remoteUpstreams := make(map[structs.PeeredServiceName]structs.Upstream)

View File

@ -35,6 +35,7 @@ func Test_MergeServiceConfig_TransparentProxy(t *testing.T) {
ProxyConfig: map[string]interface{}{
"foo": "bar",
},
MutualTLSMode: structs.MutualTLSModePermissive,
Expose: structs.ExposeConfig{
Checks: true,
Paths: []structs.ExposePath{
@ -76,6 +77,7 @@ func Test_MergeServiceConfig_TransparentProxy(t *testing.T) {
OutboundListenerPort: 10101,
DialedDirectly: true,
},
MutualTLSMode: structs.MutualTLSModePermissive,
Config: map[string]interface{}{
"foo": "bar",
},

View File

@ -47,6 +47,7 @@ func ComputeResolvedServiceConfig(
thisReply.ProxyConfig = mapCopy.(map[string]interface{})
thisReply.Mode = proxyConf.Mode
thisReply.TransparentProxy = proxyConf.TransparentProxy
thisReply.MutualTLSMode = proxyConf.MutualTLSMode
thisReply.MeshGateway = proxyConf.MeshGateway
thisReply.Expose = proxyConf.Expose
thisReply.EnvoyExtensions = proxyConf.EnvoyExtensions
@ -120,6 +121,10 @@ func ComputeResolvedServiceConfig(
thisReply.ProxyConfig = proxyConf
}
if serviceConf.MutualTLSMode != structs.MutualTLSModeDefault {
thisReply.MutualTLSMode = serviceConf.MutualTLSMode
}
thisReply.Meta = serviceConf.Meta
// Service defaults' envoy extensions are appended to the proxy defaults extensions so that proxy defaults
// extensions are applied first.

View File

@ -494,6 +494,50 @@ func Test_ComputeResolvedServiceConfig(t *testing.T) {
},
},
},
{
name: "service-defaults inherits mutual_tls_mode from proxy-defaults",
args: args{
scReq: &structs.ServiceConfigRequest{
Name: "sid",
},
entries: &ResolvedServiceConfigSet{
ProxyDefaults: map[string]*structs.ProxyConfigEntry{
acl.DefaultEnterpriseMeta().PartitionOrDefault(): {
MutualTLSMode: structs.MutualTLSModePermissive,
},
},
ServiceDefaults: map[structs.ServiceID]*structs.ServiceConfigEntry{
sid: {},
},
},
},
want: &structs.ServiceConfigResponse{
MutualTLSMode: structs.MutualTLSModePermissive,
},
},
{
name: "service-defaults overrides mutual_tls_mode in proxy-defaults",
args: args{
scReq: &structs.ServiceConfigRequest{
Name: "sid",
},
entries: &ResolvedServiceConfigSet{
ProxyDefaults: map[string]*structs.ProxyConfigEntry{
acl.DefaultEnterpriseMeta().PartitionOrDefault(): {
MutualTLSMode: structs.MutualTLSModeStrict,
},
},
ServiceDefaults: map[structs.ServiceID]*structs.ServiceConfigEntry{
sid: {
MutualTLSMode: structs.MutualTLSModePermissive,
},
},
},
},
want: &structs.ServiceConfigResponse{
MutualTLSMode: structs.MutualTLSModePermissive,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -18,6 +18,10 @@ import (
"github.com/hashicorp/consul/lib/maps"
)
var (
permissiveModeNotAllowedError = errors.New("cannot set MutualTLSMode=permissive because AllowEnablingPermissiveMutualTLS=false in the mesh config entry")
)
type ConfigEntryLinkIndex struct {
}
@ -229,14 +233,16 @@ func ensureConfigEntryTxn(tx WriteTxn, idx uint64, statusUpdate bool, conf struc
return fmt.Errorf("failed configuration lookup: %s", err)
}
var existingConf structs.ConfigEntry
raftIndex := conf.GetRaftIndex()
if existing != nil {
existingIdx := existing.(structs.ConfigEntry).GetRaftIndex()
existingConf = existing.(structs.ConfigEntry)
existingIdx := existingConf.GetRaftIndex()
raftIndex.CreateIndex = existingIdx.CreateIndex
// Handle optional upsert logic.
if updatableConf, ok := conf.(structs.UpdatableConfigEntry); ok {
if err := updatableConf.UpdateOver(existing.(structs.ConfigEntry)); err != nil {
if err := updatableConf.UpdateOver(existingConf); err != nil {
return err
}
}
@ -256,7 +262,7 @@ func ensureConfigEntryTxn(tx WriteTxn, idx uint64, statusUpdate bool, conf struc
}
raftIndex.ModifyIndex = idx
err = validateProposedConfigEntryInGraph(tx, q, conf)
err = validateProposedConfigEntryInGraph(tx, q, conf, existingConf)
if err != nil {
return err // Err is already sufficiently decorated.
}
@ -445,7 +451,7 @@ func deleteConfigEntryTxn(tx WriteTxn, idx uint64, kind, name string, entMeta *a
}
}
err = validateProposedConfigEntryInGraph(tx, q, nil)
err = validateProposedConfigEntryInGraph(tx, q, nil, c)
if err != nil {
return err // Err is already sufficiently decorated.
}
@ -544,7 +550,7 @@ func insertConfigEntryWithTxn(tx WriteTxn, idx uint64, conf structs.ConfigEntry)
func validateProposedConfigEntryInGraph(
tx ReadTxn,
kindName configentry.KindName,
newEntry structs.ConfigEntry,
newEntry, existingEntry structs.ConfigEntry,
) error {
switch kindName.Kind {
case structs.ProxyDefaults:
@ -552,7 +558,25 @@ func validateProposedConfigEntryInGraph(
if kindName.Name != structs.ProxyConfigGlobal {
return nil
}
if newPD, ok := newEntry.(*structs.ProxyConfigEntry); ok && newPD != nil {
var existingMode structs.MutualTLSMode
if existingPD, ok := existingEntry.(*structs.ProxyConfigEntry); ok && existingPD != nil {
existingMode = existingPD.MutualTLSMode
}
if err := checkMutualTLSMode(tx, kindName, newPD.MutualTLSMode, existingMode); err != nil {
return err
}
}
case structs.ServiceDefaults:
if newSD, ok := newEntry.(*structs.ServiceConfigEntry); ok && newSD != nil {
var existingMode structs.MutualTLSMode
if existingSD, ok := existingEntry.(*structs.ServiceConfigEntry); ok && existingSD != nil {
existingMode = existingSD.MutualTLSMode
}
if err := checkMutualTLSMode(tx, kindName, newSD.MutualTLSMode, existingMode); err != nil {
return err
}
}
case structs.ServiceRouter:
case structs.ServiceSplitter:
case structs.ServiceResolver:
@ -583,6 +607,54 @@ func validateProposedConfigEntryInGraph(
return validateProposedConfigEntryInServiceGraph(tx, kindName, newEntry)
}
// checkMutualTLSMode validates the MutualTLSMode (in proxy-defaults or
// service-defaults) against the AllowEnablingPermissiveMutualTLS setting in the
// mesh config entry, as follows:
//
// - If AllowEnablingPermissiveMutualTLS=true, any value of MutualTLSMode is allowed.
// - If AllowEnablingPermissiveMutualTLS=false, *changing* to MutualTLSMode=permissive is not allowed
//
// If MutualTLSMode=permissive is already stored, but the setting is not being changed
// by this transaction, then the permissive setting is allowed (does not cause a validation error).
func checkMutualTLSMode(tx ReadTxn, kindName configentry.KindName, newMode, existingMode structs.MutualTLSMode) error {
// Setting the mode to something not permissive is always allowed.
if newMode != structs.MutualTLSModePermissive {
return nil
}
// If the MutualTLSMode has not been changed, then do not error. This allows
// remaining in MutualTLSMode=permissive without causing validation failures
// after AllowEnablingPermissiveMutualTLS=false is set.
if existingMode == newMode {
return nil
}
// The mesh config entry exists in the default namespace in the given partition.
metaInDefaultNS := acl.NewEnterpriseMetaWithPartition(
kindName.EnterpriseMeta.PartitionOrDefault(),
acl.DefaultNamespaceName,
)
_, mesh, err := configEntryTxn(tx, nil, structs.MeshConfig, structs.MeshConfigMesh, &metaInDefaultNS)
if err != nil {
return fmt.Errorf("unable to validate MutualTLSMode against mesh config entry: %w", err)
}
permissiveAllowed := false
if mesh != nil {
meshConfig, ok := mesh.(*structs.MeshConfigEntry)
if !ok {
return fmt.Errorf("unable to validate MutualTLSMode: invalid type from mesh config entry lookup: %T", mesh)
}
permissiveAllowed = meshConfig.AllowEnablingPermissiveMutualTLS
}
// If permissive is not allowed, then any value for MutualTLSMode is allowed.
if !permissiveAllowed && newMode == structs.MutualTLSModePermissive {
return permissiveModeNotAllowedError
}
return nil
}
func checkGatewayClash(tx ReadTxn, kindName configentry.KindName, otherKind string) error {
_, entry, err := configEntryTxn(tx, nil, otherKind, kindName.Name, &kindName.EnterpriseMeta)
if err != nil {

View File

@ -3182,3 +3182,210 @@ func TestStateStore_ConfigEntry_VirtualIP(t *testing.T) {
})
}
}
func TestStore_MutualTLSMode_Validation_InitialWrite(t *testing.T) {
cases := []struct {
// setup
mesh *structs.MeshConfigEntry
mtlsMode structs.MutualTLSMode
expErr error
}{
// Mesh config entry does not exist. Should default to AllowEnablingPermissiveMutualTLS=false.
{
mtlsMode: structs.MutualTLSModeDefault,
},
{
mtlsMode: structs.MutualTLSModeStrict,
},
{
mtlsMode: structs.MutualTLSModePermissive,
expErr: permissiveModeNotAllowedError,
},
// Mesh config entry contains AllowEnablingPermissiveMutualTLS=false
{
mesh: &structs.MeshConfigEntry{},
mtlsMode: structs.MutualTLSModeDefault,
},
{
mesh: &structs.MeshConfigEntry{},
mtlsMode: structs.MutualTLSModeStrict,
},
{
mesh: &structs.MeshConfigEntry{},
mtlsMode: structs.MutualTLSModePermissive,
expErr: permissiveModeNotAllowedError,
},
// Mesh config entry exists with AllowEnablingPermissiveMutualTLS=true.
{
mesh: &structs.MeshConfigEntry{AllowEnablingPermissiveMutualTLS: true},
mtlsMode: structs.MutualTLSModeDefault,
},
{
mesh: &structs.MeshConfigEntry{AllowEnablingPermissiveMutualTLS: true},
mtlsMode: structs.MutualTLSModeStrict,
},
{
mesh: &structs.MeshConfigEntry{AllowEnablingPermissiveMutualTLS: true},
mtlsMode: structs.MutualTLSModePermissive,
},
}
for _, c := range cases {
c := c
var name string
if c.mesh == nil {
name = fmt.Sprintf("when mesh config entry not found")
} else {
name = fmt.Sprintf("when AllowEnablingPermissiveMutualTLS=%v", c.mesh.AllowEnablingPermissiveMutualTLS)
}
if c.expErr != nil {
name += " cannot"
} else {
name += " can"
}
name += fmt.Sprintf(" set MutualTLSMode=%q", c.mtlsMode)
t.Run(name, func(t *testing.T) {
s := testConfigStateStore(t)
var err error
var idx uint64
if c.mesh != nil {
idx, err = writeConfigAndBumpIndexForTest(s, idx, c.mesh)
require.NoError(t, err)
}
idx, err = writeConfigAndBumpIndexForTest(s, idx, &structs.ProxyConfigEntry{
Kind: structs.ProxyDefaults,
Name: structs.ProxyConfigGlobal,
MutualTLSMode: c.mtlsMode,
})
require.Equal(t, c.expErr, err)
_, err = writeConfigAndBumpIndexForTest(s, idx, &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "test-svc",
MutualTLSMode: c.mtlsMode,
})
require.Equal(t, c.expErr, err)
})
}
}
func TestStore_MutualTLSMode_Validation_SubsequentWrite(t *testing.T) {
cases := []struct {
allowPermissive bool
initialModes []structs.MutualTLSMode
transitions map[structs.MutualTLSMode]error
}{
{
allowPermissive: false,
initialModes: []structs.MutualTLSMode{
structs.MutualTLSModeDefault,
structs.MutualTLSModeStrict,
},
transitions: map[structs.MutualTLSMode]error{
structs.MutualTLSModeDefault: nil,
structs.MutualTLSModeStrict: nil,
// Cannot transition from "" -> "permissive"
// Cannot transition from "strict" -> "permissive"
structs.MutualTLSModePermissive: permissiveModeNotAllowedError,
},
},
{
allowPermissive: false,
initialModes: []structs.MutualTLSMode{
structs.MutualTLSModePermissive,
},
transitions: map[structs.MutualTLSMode]error{
structs.MutualTLSModeDefault: nil,
structs.MutualTLSModeStrict: nil,
// Can transition from "permissive" -> "permissive"
structs.MutualTLSModePermissive: nil,
},
},
{
allowPermissive: true,
initialModes: []structs.MutualTLSMode{
structs.MutualTLSModeDefault,
structs.MutualTLSModeStrict,
structs.MutualTLSModePermissive,
},
transitions: map[structs.MutualTLSMode]error{
// Can transition from any mode to any other mode when allowPermissive=true
structs.MutualTLSModeDefault: nil,
structs.MutualTLSModeStrict: nil,
structs.MutualTLSModePermissive: nil,
},
},
}
for _, c := range cases {
c := c
for _, initialMode := range c.initialModes {
for newMode, expErr := range c.transitions {
name := fmt.Sprintf("when AllowEnablingPermissiveMutualTLS=%v", c.allowPermissive)
if expErr != nil {
name += " cannot"
} else {
name += " can"
}
name += fmt.Sprintf(" transition MutualTLSMode from %q to %q", initialMode, newMode)
t.Run(name, func(t *testing.T) {
s := testConfigStateStore(t)
// Setup initial state.
idx, err := writeConfigAndBumpIndexForTest(s, 0, &structs.MeshConfigEntry{
AllowEnablingPermissiveMutualTLS: true, // set to true to allow writing any initial mode.
})
require.NoError(t, err)
idx, err = writeConfigAndBumpIndexForTest(s, idx, &structs.ProxyConfigEntry{
Kind: structs.ProxyDefaults,
Name: structs.ProxyConfigGlobal,
MutualTLSMode: initialMode,
})
require.NoError(t, err)
idx, err = writeConfigAndBumpIndexForTest(s, idx, &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "test-svc",
MutualTLSMode: initialMode,
})
require.NoError(t, err)
// Set AllowEnablingPermissiveMutualTLS for the test case.
idx, err = writeConfigAndBumpIndexForTest(s, idx, &structs.MeshConfigEntry{
AllowEnablingPermissiveMutualTLS: c.allowPermissive,
})
require.NoError(t, err)
// Test switching to the other mode.
idx, err = writeConfigAndBumpIndexForTest(s, idx, &structs.ProxyConfigEntry{
Kind: structs.ProxyDefaults,
Name: structs.ProxyConfigGlobal,
MutualTLSMode: newMode,
})
require.Equal(t, expErr, err)
_, err = writeConfigAndBumpIndexForTest(s, idx, &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "test-svc",
MutualTLSMode: newMode,
})
require.Equal(t, expErr, err)
})
}
}
}
}
func writeConfigAndBumpIndexForTest(s *Store, idx uint64, entry structs.ConfigEntry) (uint64, error) {
err := s.EnsureConfigEntry(idx, entry)
if err == nil {
idx++
}
return idx, err
}

View File

@ -125,6 +125,26 @@ type WarningConfigEntry interface {
ConfigEntry
}
type MutualTLSMode string
const (
MutualTLSModeDefault MutualTLSMode = ""
MutualTLSModeStrict MutualTLSMode = "strict"
MutualTLSModePermissive MutualTLSMode = "permissive"
)
func (m MutualTLSMode) validate() error {
switch m {
case MutualTLSModeDefault, MutualTLSModeStrict, MutualTLSModePermissive:
return nil
}
return fmt.Errorf("Invalid MutualTLSMode %q. Must be one of %q, %q, or %q.", m,
MutualTLSModeDefault,
MutualTLSModeStrict,
MutualTLSModePermissive,
)
}
// ServiceConfiguration is the top-level struct for the configuration of a service
// across the entire cluster.
type ServiceConfigEntry struct {
@ -133,6 +153,7 @@ type ServiceConfigEntry struct {
Protocol string
Mode ProxyMode `json:",omitempty"`
TransparentProxy TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"`
MutualTLSMode MutualTLSMode `json:",omitempty" alias:"mutual_tls_mode"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
ExternalSNI string `json:",omitempty" alias:"external_sni"`
@ -267,6 +288,10 @@ func (e *ServiceConfigEntry) Validate() error {
validationErr = multierror.Append(validationErr, err)
}
if err := e.MutualTLSMode.validate(); err != nil {
return err
}
return validationErr
}
@ -372,6 +397,7 @@ type ProxyConfigEntry struct {
Config map[string]interface{}
Mode ProxyMode `json:",omitempty"`
TransparentProxy TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"`
MutualTLSMode MutualTLSMode `json:",omitempty" alias:"mutual_tls_mode"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
AccessLogs AccessLogsConfig `json:",omitempty" alias:"access_logs"`
@ -452,6 +478,10 @@ func (e *ProxyConfigEntry) Validate() error {
return err
}
if err := e.MutualTLSMode.validate(); err != nil {
return err
}
return e.validateEnterpriseMeta()
}
@ -1157,6 +1187,7 @@ type ServiceConfigResponse struct {
MeshGateway MeshGatewayConfig `json:",omitempty"`
Expose ExposeConfig `json:",omitempty"`
TransparentProxy TransparentProxyConfig `json:",omitempty"`
MutualTLSMode MutualTLSMode `json:",omitempty"`
Mode ProxyMode `json:",omitempty"`
Destination DestinationConfig `json:",omitempty"`
AccessLogs AccessLogsConfig `json:",omitempty"`

View File

@ -16,6 +16,10 @@ type MeshConfigEntry struct {
// when enabled.
TransparentProxy TransparentProxyMeshConfig `alias:"transparent_proxy"`
// AllowEnablingPermissiveMutualTLS must be true in order to allow setting
// MutualTLSMode=permissive in either service-defaults or proxy-defaults.
AllowEnablingPermissiveMutualTLS bool `json:",omitempty" alias:"allow_enabling_permissive_mutual_tls"`
TLS *MeshTLSConfig `json:",omitempty"`
HTTP *MeshHTTPConfig `json:",omitempty"`

View File

@ -350,6 +350,7 @@ func TestDecodeConfigEntry(t *testing.T) {
mesh_gateway {
mode = "remote"
}
mutual_tls_mode = "permissive"
`,
camel: `
Kind = "proxy-defaults"
@ -369,6 +370,7 @@ func TestDecodeConfigEntry(t *testing.T) {
MeshGateway {
Mode = "remote"
}
MutualTLSMode = "permissive"
`,
expect: &ProxyConfigEntry{
Kind: "proxy-defaults",
@ -388,6 +390,7 @@ func TestDecodeConfigEntry(t *testing.T) {
MeshGateway: MeshGatewayConfig{
Mode: MeshGatewayModeRemote,
},
MutualTLSMode: MutualTLSModePermissive,
},
},
{
@ -404,6 +407,7 @@ func TestDecodeConfigEntry(t *testing.T) {
mesh_gateway {
mode = "remote"
}
mutual_tls_mode = "permissive"
balance_inbound_connections = "exact_balance"
upstream_config {
overrides = [
@ -447,6 +451,7 @@ func TestDecodeConfigEntry(t *testing.T) {
MeshGateway {
Mode = "remote"
}
MutualTLSMode = "permissive"
BalanceInboundConnections = "exact_balance"
UpstreamConfig {
Overrides = [
@ -490,6 +495,7 @@ func TestDecodeConfigEntry(t *testing.T) {
MeshGateway: MeshGatewayConfig{
Mode: MeshGatewayModeRemote,
},
MutualTLSMode: MutualTLSModePermissive,
BalanceInboundConnections: "exact_balance",
UpstreamConfig: &UpstreamConfiguration{
Overrides: []*UpstreamConfig{
@ -1811,6 +1817,7 @@ func TestDecodeConfigEntry(t *testing.T) {
transparent_proxy {
mesh_destinations_only = true
}
allow_enabling_permissive_mutual_tls = true
tls {
incoming {
tls_min_version = "TLSv1_1"
@ -1845,6 +1852,7 @@ func TestDecodeConfigEntry(t *testing.T) {
TransparentProxy {
MeshDestinationsOnly = true
}
AllowEnablingPermissiveMutualTLS = true
TLS {
Incoming {
TLSMinVersion = "TLSv1_1"
@ -1878,6 +1886,7 @@ func TestDecodeConfigEntry(t *testing.T) {
TransparentProxy: TransparentProxyMeshConfig{
MeshDestinationsOnly: true,
},
AllowEnablingPermissiveMutualTLS: true,
TLS: &MeshTLSConfig{
Incoming: &MeshDirectionalTLSConfig{
TLSMinVersion: types.TLSv1_1,
@ -2840,6 +2849,22 @@ func TestServiceConfigEntry(t *testing.T) {
},
},
},
"validate: invalid MutualTLSMode in service-defaults": {
entry: &ServiceConfigEntry{
Kind: ServiceDefaults,
Name: "web",
MutualTLSMode: MutualTLSMode("invalid-mtls-mode"),
},
validateErr: `Invalid MutualTLSMode "invalid-mtls-mode". Must be one of "", "strict", or "permissive".`,
},
"validate: invalid MutualTLSMode in proxy-defaults": {
entry: &ServiceConfigEntry{
Kind: ProxyDefaults,
Name: ProxyConfigGlobal,
MutualTLSMode: MutualTLSMode("invalid-mtls-mode"),
},
validateErr: `Invalid MutualTLSMode "invalid-mtls-mode". Must be one of "", "strict", or "permissive".`,
},
}
testConfigEntryNormalizeAndValidate(t, cases)
}

View File

@ -289,6 +289,9 @@ type ConnectProxyConfig struct {
// transparent mode.
TransparentProxy TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"`
// MutualTLSMode allows configuring the proxy to allow non-mTLS traffic.
MutualTLSMode MutualTLSMode `json:"-" bexpr:"-"`
// AccessLogs configures the output and format of Envoy access logs
AccessLogs AccessLogsConfig `json:",omitempty" alias:"access_logs"`
}

View File

@ -1430,9 +1430,52 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
return nil, fmt.Errorf("failed to attach Consul filters and TLS context to custom public listener: %v", err)
}
// When permissive mTLS mode is enabled, include an additional filter chain
// that matches on the `destination_port == <service port>`. Traffic sent
// directly to the service port is passed through to the application
// unmodified.
if cfgSnap.Proxy.MutualTLSMode == structs.MutualTLSModePermissive {
chain, err := makePermissiveFilterChain(cfgSnap, filterOpts)
if err != nil {
return nil, fmt.Errorf("unable to add permissive mtls filter chain: %w", err)
}
if chain == nil {
s.Logger.Debug("no service port defined for service in permissive mTLS mode; not adding filter chain for non-mTLS traffic")
} else {
l.FilterChains = append(l.FilterChains, chain)
// With tproxy, the REDIRECT iptables target rewrites the destination ip/port
// to the proxy ip/port (e.g. 127.0.0.1:20000) for incoming packets.
// We need the original_dst filter to recover the original destination address.
l.UseOriginalDst = &wrapperspb.BoolValue{Value: true}
}
}
return l, err
}
func makePermissiveFilterChain(cfgSnap *proxycfg.ConfigSnapshot, opts listenerFilterOpts) (*envoy_listener_v3.FilterChain, error) {
servicePort := cfgSnap.Proxy.LocalServicePort
if servicePort <= 0 {
// No service port means the service does not accept incoming traffic, so
// the connect proxy does not need to listen for incoming non-mTLS traffic.
return nil, nil
}
opts.statPrefix += "permissive_"
filter, err := makeTCPProxyFilter(opts)
if err != nil {
return nil, err
}
chain := &envoy_listener_v3.FilterChain{
FilterChainMatch: &envoy_listener_v3.FilterChainMatch{
DestinationPort: &wrapperspb.UInt32Value{Value: uint32(servicePort)},
},
Filters: []*envoy_listener_v3.Filter{filter},
}
return chain, nil
}
// finalizePublicListenerFromConfig is used for best-effort injection of Consul filter-chains onto listeners.
// This include L4 authorization filters and TLS context.
func (s *ResourceGenerator) finalizePublicListenerFromConfig(l *envoy_listener_v3.Listener, cfgSnap *proxycfg.ConfigSnapshot, useHTTPFilter bool) error {

View File

@ -1099,6 +1099,16 @@ func TestListenersFromSnapshot(t *testing.T) {
nil)
},
},
{
name: "connect-proxy-with-tproxy-and-permissive-mtls",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) {
ns.Proxy.MutualTLSMode = structs.MutualTLSModePermissive
ns.Proxy.Mode = structs.ProxyModeTransparent
},
nil)
},
},
}
tests = append(tests, makeListenerDiscoChainTests(false)...)

View File

@ -0,0 +1,162 @@
{
"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": "outbound_listener:127.0.0.1:15001",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 15001
}
},
"defaultFilterChain": {
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "upstream.original-destination",
"cluster": "original-destination"
}
}
]
},
"listenerFilters": [
{
"name": "envoy.filters.listener.original_dst",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst"
}
}
],
"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.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {},
"statPrefix": "connect_authz"
}
},
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "public_listener",
"cluster": "local_app"
}
}
],
"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
}
}
},
{
"filterChainMatch": {
"destinationPort": 8080
},
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "permissive_public_listener",
"cluster": "local_app"
}
}
]
}
],
"useOriginalDst": true,
"trafficDirection": "INBOUND"
}
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"nonce": "00000001"
}

View File

@ -109,6 +109,21 @@ type TransparentProxyConfig struct {
DialedDirectly bool `json:",omitempty" alias:"dialed_directly"`
}
type MutualTLSMode string
const (
// MutualTLSModeDefault represents no specific mode and should
// be used to indicate that a different layer of the configuration
// chain should take precedence.
MutualTLSModeDefault MutualTLSMode = ""
// MutualTLSModeStrict requires mTLS for incoming traffic.
MutualTLSModeStrict MutualTLSMode = "strict"
// MutualTLSModePermissive allows incoming non-mTLS traffic.
MutualTLSModePermissive MutualTLSMode = "permissive"
)
// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect.
// Users can expose individual paths and/or all HTTP/GRPC paths for checks.
type ExposeConfig struct {
@ -290,6 +305,7 @@ type ServiceConfigEntry struct {
Protocol string `json:",omitempty"`
Mode ProxyMode `json:",omitempty"`
TransparentProxy *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"`
MutualTLSMode MutualTLSMode `json:",omitempty" alias:"mutual_tls_mode"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
ExternalSNI string `json:",omitempty" alias:"external_sni"`
@ -320,6 +336,7 @@ type ProxyConfigEntry struct {
Namespace string `json:",omitempty"`
Mode ProxyMode `json:",omitempty"`
TransparentProxy *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"`
MutualTLSMode MutualTLSMode `json:",omitempty" alias:"mutual_tls_mode"`
Config map[string]interface{} `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`

View File

@ -22,6 +22,10 @@ type MeshConfigEntry struct {
// in transparent mode.
TransparentProxy TransparentProxyMeshConfig `alias:"transparent_proxy"`
// AllowEnablingPermissiveMutualTLS must be true in order to allow setting
// MutualTLSMode=permissive in either service-defaults or proxy-defaults.
AllowEnablingPermissiveMutualTLS bool `json:",omitempty" alias:"allow_enabling_permissive_mutual_tls"`
TLS *MeshTLSConfig `json:",omitempty"`
HTTP *MeshHTTPConfig `json:",omitempty"`

View File

@ -28,6 +28,7 @@ func TestAPI_ConfigEntries(t *testing.T) {
"foo": "bar",
"bar": 1.0,
},
MutualTLSMode: MutualTLSModeStrict,
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
@ -52,7 +53,8 @@ func TestAPI_ConfigEntries(t *testing.T) {
require.Equal(t, global_proxy.Kind, readProxy.Kind)
require.Equal(t, global_proxy.Name, readProxy.Name)
require.Equal(t, global_proxy.Config, readProxy.Config)
require.Equal(t, global_proxy.Meta, readProxy.Meta)
require.Equal(t, global_proxy.MutualTLSMode, readProxy.MutualTLSMode)
require.Equal(t, global_proxy.Meta, readProxy.GetMeta())
require.Equal(t, global_proxy.Meta, readProxy.GetMeta())
global_proxy.Config["baz"] = true
@ -103,6 +105,7 @@ func TestAPI_ConfigEntries(t *testing.T) {
Kind: ServiceDefaults,
Name: "foo",
Protocol: "udp",
MutualTLSMode: MutualTLSModeStrict,
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
@ -149,6 +152,7 @@ func TestAPI_ConfigEntries(t *testing.T) {
require.Equal(t, service.Kind, readService.Kind)
require.Equal(t, service.Name, readService.Name)
require.Equal(t, service.Protocol, readService.Protocol)
require.Equal(t, service.MutualTLSMode, readService.MutualTLSMode)
require.Equal(t, service.Meta, readService.Meta)
require.Equal(t, service.Meta, readService.GetMeta())
require.Equal(t, service.MaxInboundConnections, readService.MaxInboundConnections)
@ -220,6 +224,7 @@ func TestAPI_ConfigEntries(t *testing.T) {
t.Run("Mesh", func(t *testing.T) {
mesh := &MeshConfigEntry{
TransparentProxy: TransparentProxyMeshConfig{MeshDestinationsOnly: true},
AllowEnablingPermissiveMutualTLS: true,
Meta: map[string]string{
"foo": "bar",
"gir": "zim",

View File

@ -1035,6 +1035,7 @@ func MeshConfigToStructs(s *MeshConfig, t *structs.MeshConfigEntry) {
if s.TransparentProxy != nil {
TransparentProxyMeshConfigToStructs(s.TransparentProxy, &t.TransparentProxy)
}
t.AllowEnablingPermissiveMutualTLS = s.AllowEnablingPermissiveMutualTLS
if s.TLS != nil {
var x structs.MeshTLSConfig
MeshTLSConfigToStructs(s.TLS, &x)
@ -1061,6 +1062,7 @@ func MeshConfigFromStructs(t *structs.MeshConfigEntry, s *MeshConfig) {
TransparentProxyMeshConfigFromStructs(&t.TransparentProxy, &x)
s.TransparentProxy = &x
}
s.AllowEnablingPermissiveMutualTLS = t.AllowEnablingPermissiveMutualTLS
if t.TLS != nil {
var x MeshTLSConfig
MeshTLSConfigFromStructs(t.TLS, &x)
@ -1269,6 +1271,7 @@ func ServiceDefaultsToStructs(s *ServiceDefaults, t *structs.ServiceConfigEntry)
if s.TransparentProxy != nil {
TransparentProxyConfigToStructs(s.TransparentProxy, &t.TransparentProxy)
}
t.MutualTLSMode = mutualTLSModeToStructs(s.MutualTLSMode)
if s.MeshGateway != nil {
MeshGatewayConfigToStructs(s.MeshGateway, &t.MeshGateway)
}
@ -1304,6 +1307,7 @@ func ServiceDefaultsFromStructs(t *structs.ServiceConfigEntry, s *ServiceDefault
TransparentProxyConfigFromStructs(&t.TransparentProxy, &x)
s.TransparentProxy = &x
}
s.MutualTLSMode = mutualTLSModeFromStructs(t.MutualTLSMode)
{
var x MeshGatewayConfig
MeshGatewayConfigFromStructs(&t.MeshGateway, &x)

View File

@ -340,6 +340,32 @@ func proxyModeToStructs(a ProxyMode) structs.ProxyMode {
}
}
func mutualTLSModeFromStructs(a structs.MutualTLSMode) MutualTLSMode {
switch a {
case structs.MutualTLSModeDefault:
return MutualTLSMode_MutualTLSModeDefault
case structs.MutualTLSModeStrict:
return MutualTLSMode_MutualTLSModeStrict
case structs.MutualTLSModePermissive:
return MutualTLSMode_MutualTLSModePermissive
default:
return MutualTLSMode_MutualTLSModeDefault
}
}
func mutualTLSModeToStructs(a MutualTLSMode) structs.MutualTLSMode {
switch a {
case MutualTLSMode_MutualTLSModeDefault:
return structs.MutualTLSModeDefault
case MutualTLSMode_MutualTLSModeStrict:
return structs.MutualTLSModeStrict
case MutualTLSMode_MutualTLSModePermissive:
return structs.MutualTLSModePermissive
default:
return structs.MutualTLSModeDefault
}
}
func meshGatewayModeFromStructs(a structs.MeshGatewayMode) MeshGatewayMode {
switch a {
case structs.MeshGatewayModeDefault:

File diff suppressed because it is too large Load Diff

View File

@ -58,6 +58,7 @@ message MeshConfig {
MeshHTTPConfig HTTP = 3;
map<string, string> Meta = 4;
PeeringMeshConfig Peering = 5;
bool AllowEnablingPermissiveMutualTLS = 6;
}
// mog annotation:
@ -475,6 +476,8 @@ message ServiceDefaults {
map<string, string> Meta = 13;
// mog: func-to=EnvoyExtensionsToStructs func-from=EnvoyExtensionsFromStructs
repeated hashicorp.consul.internal.common.EnvoyExtension EnvoyExtensions = 14;
// mog: func-to=mutualTLSModeToStructs func-from=mutualTLSModeFromStructs
MutualTLSMode MutualTLSMode = 15;
}
enum ProxyMode {
@ -483,6 +486,12 @@ enum ProxyMode {
ProxyModeDirect = 2;
}
enum MutualTLSMode {
MutualTLSModeDefault = 0;
MutualTLSModeStrict = 1;
MutualTLSModePermissive = 2;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.TransparentProxyConfig

View File

@ -19,7 +19,7 @@ import (
func TestNewCheckServiceNodeFromStructs_RoundTrip(t *testing.T) {
repeat(t, func(t *testing.T, fuzzer *fuzz.Fuzzer) {
fuzzer.Funcs(randInt32, randUint32, randInterface, randStructsUpstream, randEnterpriseMeta)
fuzzer.Funcs(randInt32, randUint32, randInterface, randStructsUpstream, randEnterpriseMeta, randStructsConnectProxyConfig)
var target structs.CheckServiceNode
fuzzer.Fuzz(&target)
@ -77,6 +77,19 @@ func randInt32(i *int, c fuzz.Continue) {
*i = int(c.Rand.Int31())
}
// randStructsConnectProxyConfig is a custom fuzzer function which skips
// generating values for fields enumerated in the ignore-fields annotation.
func randStructsConnectProxyConfig(p *structs.ConnectProxyConfig, c fuzz.Continue) {
v := reflect.ValueOf(p).Elem()
for i := 0; i < v.NumField(); i++ {
switch v.Type().Field(i).Name {
case "MutualTLSMode":
continue
}
c.Fuzz(v.Field(i).Addr().Interface())
}
}
// randStructsUpstream is a custom fuzzer function which skips generating values
// for fields enumerated in the ignore-fields annotation.
func randStructsUpstream(u *structs.Upstream, c fuzz.Continue) {

View File

@ -36,6 +36,7 @@ const (
// target=github.com/hashicorp/consul/agent/structs.ConnectProxyConfig
// output=service.gen.go
// name=Structs
// ignore-fields=MutualTLSMode
type ConnectProxyConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache

View File

@ -20,6 +20,7 @@ import "private/pbservice/healthcheck.proto";
// target=github.com/hashicorp/consul/agent/structs.ConnectProxyConfig
// output=service.gen.go
// name=Structs
// ignore-fields=MutualTLSMode
message ConnectProxyConfig {
// DestinationServiceName is required and is the name of the service to accept
// traffic for.